From d37ef22184489b86b37a47bcd981bd40cf396a73 Mon Sep 17 00:00:00 2001 From: Jeff Posluns Date: Mon, 19 Oct 2026 09:06:16 -0400 Subject: [PATCH] Two-section layout, batch 5: 19 guides; mfa.md header order Headline-first order (order-p4e, carried note H5) and --migrate; no body or digest change. authentication.md follows after #698. --- CHANGELOG.md | 1 + VERSION | 2 +- admin-uis.md | 420 +- bi-dashboards.md | 364 +- deployment-lifecycle.md | 440 +- devops-uis.md | 696 +- dotnet.md | 642 +- frontend-frameworks.md | 466 +- gpu-clouds.md | 464 +- low-code-builders.md | 616 +- mfa.md | 52 +- mlflow.md | 422 +- nextjs.md | 388 +- paas.md | 442 +- php.md | 514 +- plugin/plugin.json | 2 +- .../references/MANIFEST.sha256 | 40 +- .../references/admin-uis.md | 420 +- .../references/bi-dashboards.md | 364 +- .../references/deployment-lifecycle.md | 440 +- .../references/devops-uis.md | 696 +- .../references/dotnet.md | 642 +- .../references/frontend-frameworks.md | 466 +- .../references/gpu-clouds.md | 464 +- .../references/low-code-builders.md | 616 +- .../references/mfa.md | 52 +- .../references/mlflow.md | 422 +- .../references/nextjs.md | 388 +- .../references/paas.md | 442 +- .../references/php.md | 514 +- .../references/python.md | 618 +- .../references/ruby.md | 368 +- .../references/server-admin-panels.md | 420 +- .../references/sqlite-http-frontends.md | 686 +- .../references/sqlite.md | 696 +- .../references/transactional-email-posture.md | 446 +- .../references/web-exposure.md | 442 +- python.md | 618 +- ruby.md | 368 +- server-admin-panels.md | 420 +- site/llms-full.txt | 6818 +++++++++++++---- sqlite-http-frontends.md | 686 +- sqlite.md | 696 +- transactional-email-posture.md | 446 +- web-exposure.md | 442 +- 45 files changed, 15468 insertions(+), 10599 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f996d7d0..f758b025b 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 +- Nineteen more guides move to the two-section layout (maintainer request, 2026-10-18), with each guide's own tool named first in its header; `mfa.md`, already in the layout, now lists authentik, Keycloak and oauth2-proxy in its header (Authelia has no recorded release version, so it stays in the folded count); no guide body or digest changes. Guides: `dotnet.md`, `php.md`, `ruby.md`, `nextjs.md`, `frontend-frameworks.md`, `web-exposure.md`, `admin-uis.md`, `bi-dashboards.md`, `devops-uis.md`, `server-admin-panels.md`, `low-code-builders.md`, `python.md`, `deployment-lifecycle.md`, `gpu-clouds.md`, `mlflow.md`, `paas.md`, `sqlite.md`, `sqlite-http-frontends.md`, `transactional-email-posture.md` (#699) - 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; `workflow-orchestrators.md`, already in the layout, now lists Temporal, Flower and Argo with its other headline tools in its header; no guide body or digest changes. Guides: `host.md`, `cloud-firewalls.md`, `egress-metadata.md`, `cloudflare.md`, `tunnels.md`, `tailscale.md`, `cloud-identity-proxies.md`, `fronting-auth.md`, `mfa.md`, `identity-providers.md`, `oidc-integration.md`, `self-hosted-idp.md`, `secrets.md`, `machine-auth.md`, `connection-poolers.md`, `neo4j.md`, `surrealdb.md`, `nodejs.md`, `go.md`, `java.md` (#697) - `authentication.md`: the transport-authentication rule now labels its requirements (authenticate and check each of WebSockets, SSE, GraphQL, gRPC, webhooks, inference and management APIs on its own, not through the HTML login; verify webhook signatures and reject replays) as this guide's, keeps the Stripe signature and timestamp citations, and states that the HTML-login boundary, GraphQL, inference and management API coverage, and replay rejection beyond those checks are not established by the cited sources; those parts stay open under TODO row 1.294 (#696) - 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) diff --git a/VERSION b/VERSION index 7804b59d4..d3c11aac4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.697 +1.0.699 diff --git a/admin-uis.md b/admin-uis.md index a9d197161..03091dece 100644 --- a/admin-uis.md +++ b/admin-uis.md @@ -1,203 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "92d3d0f412f545709088bb0ce3d9b82c95988187a6fd4fcf75747d818f8d5c8b", - "components": { - "mongo": { - "name": "mongo-express", - "basis": "unknown", - "sources": { - "s69ec4eee2c85": "https://github.com/mongo-express/mongo-express" - } - }, - "grafana": { - "name": "Grafana docs", - "basis": "unknown", - "sources": { - "s7aa447405cc5": "https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/", - "sc241e0772095": "https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/" - } - }, - "prometheus": { - "name": "Prometheus docs", - "basis": "unknown", - "sources": { - "scce074c47e9c": "https://prometheus.io/docs/guides/basic-auth/", - "sc48b506cc29a": "https://prometheus.io/docs/guides/tls-encryption/" - } - }, - "phpmyadmin": { - "name": "phpMyAdmin", - "basis": "unknown", - "sources": { - "s5f6c8965c539": "https://www.phpmyadmin.net/docs/" - } - }, - "pgadmin": { - "name": "pgAdmin", - "basis": "unknown", - "sources": { - "s2f7e33714435": "https://www.pgadmin.org/docs/" - } - }, - "adminer": { - "name": "Adminer", - "basis": "unknown", - "sources": { - "sb5eda9397b18": "https://www.adminer.org/" - } - }, - "prometheus-pin": { - "name": "Prometheus", - "basis": "v3.14.0", - "sources": { - "s6d5f308b02a2": "https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425" - } - }, - "grafana-pin": { - "name": "Grafana", - "basis": "v13.2.2", - "sources": { - "s4c6ff3174e7e": "https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50", - "sb00e234d0ddd": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469", - "sf14b29976adb": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568" - } - }, - "go": { - "name": "Go net", - "basis": "unknown", - "sources": { - "s37204ff1b27c": "https://pkg.go.dev/net#Listen" - } - }, - "htpasswd": { - "name": "Apache htpasswd", - "basis": "2.4", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "httpd-source": { - "name": "Apache httpd htpasswd source", - "basis": "2.4.69", - "sources": { - "sfed14ba2991a": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297", - "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", - "s4ec6b9b216dc": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383", - "s6952d96564d9": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129" - } - }, - "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" - } - }, - "redisinsight": { - "name": "RedisInsight", - "basis": "3.8.0", - "sources": { - "s6b8fb1c9bbbc": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100", - "s7ba57be16bb3": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133", - "sddf9b5ee80b9": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49", - "s5e6e219b4b65": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35" - } - }, - "engine-pinned": { - "name": "Docker Engine networking documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "sb3dacfbaf7e1": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38", - "s1c82ff316a7e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62", - "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" - } - }, - "moby-pinned": { - "name": "Moby port mapping source", - "basis": "v28.3.3", - "sources": { - "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" - } - } - }, - "claims": { - "private": {"text": "Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice.", "components": ["grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer", "redisinsight"], "sources": ["grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18", "redisinsight:s6b8fb1c9bbbc", "redisinsight:s7ba57be16bb3", "redisinsight:sddf9b5ee80b9", "redisinsight:s5e6e219b4b65"], "status": "REASONED"}, - "mongo-enable": {"text": "Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, - "mongo-credentials": {"text": "Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, - "mongo-database": {"text": "Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, - "grafana-admin": {"text": "First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, - "grafana-anonymous": {"text": "Disable Grafana anonymous access if enabled.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, - "grafana-mfa": {"text": "Prefer SSO with MFA at the identity provider.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, - "grafana-bind": {"text": "Empty http_addr joins the port and binds all interfaces; set 127.0.0.1.", "components": ["grafana-pin", "go"], "sources": ["grafana-pin:s4c6ff3174e7e", "grafana-pin:sb00e234d0ddd", "grafana-pin:sf14b29976adb", "go:s37204ff1b27c"], "status": "REASONED"}, - "grafana-tls": {"text": "Native HTTPS uses server protocol=https, cert_file and cert_key.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5", "grafana:sc241e0772095"], "status": "REASONED"}, - "prometheus-auth": {"text": "Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes.", "components": ["prometheus"], "sources": ["prometheus:scce074c47e9c"], "status": "REASONED"}, - "prometheus-bcrypt": {"text": "Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt.", "components": ["prometheus", "htpasswd", "httpd-source", "owasp-source"], "sources": ["prometheus:scce074c47e9c", "htpasswd:scde9004bb967", "httpd-source:sfed14ba2991a", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4ec6b9b216dc", "httpd-source:s6952d96564d9", "owasp-source:s4531e65b6502"], "status": "REASONED"}, - "prometheus-bind": {"text": "Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090.", "components": ["prometheus-pin"], "sources": ["prometheus-pin:s6d5f308b02a2"], "status": "REASONED"}, - "prometheus-tls": {"text": "web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config.", "components": ["prometheus"], "sources": ["prometheus:sc48b506cc29a"], "status": "REASONED"}, - "database-panels": {"text": "Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated.", "components": ["phpmyadmin", "pgadmin"], "sources": ["phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435"], "status": "REASONED"}, - "pgadmin-login": {"text": "pgAdmin server mode has its own accounts/login.", "components": ["pgadmin"], "sources": ["pgadmin:s2f7e33714435"], "status": "REASONED"}, - "adminer-login": {"text": "Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, - "adminer-file": {"text": "Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, - "verify-listeners": {"text": "Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy.", "components": ["grafana-pin", "prometheus-pin", "engine-pinned", "moby-pinned"], "sources": ["grafana-pin:s4c6ff3174e7e", "prometheus-pin:s6d5f308b02a2", "engine-pinned:sb3dacfbaf7e1", "engine-pinned:s1c82ff316a7e", "engine-pinned:sc244607b20e4", "moby-pinned:seaf4197aafaa"], "status": "REASONED", "verify": [1]}, - "verify-login": {"text": "Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]}, - "verify-data": {"text": "A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]} - } -} ---- # Admin panels: phpMyAdmin, pgAdmin, mongo-express, Grafana, Prometheus - -**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): - -- mongo: mongo-express (unknown) -- grafana: Grafana docs (unknown) -- prometheus: Prometheus docs (unknown) -- phpmyadmin: phpMyAdmin (unknown) -- pgadmin: pgAdmin (unknown) -- adminer: Adminer (unknown) -- prometheus-pin: Prometheus (v3.14.0) -- grafana-pin: Grafana (v13.2.2) -- go: Go net (unknown) -- htpasswd: Apache htpasswd (2.4) -- httpd-source: Apache httpd htpasswd source (2.4.69) -- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) -- redisinsight: RedisInsight (3.8.0) -- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- moby-pinned: Moby port mapping source (v28.3.3) - -Claims (full source mapping in the front matter): +Written against phpMyAdmin (no recorded version), Grafana v13.2.2, Prometheus v3.14.0, RedisInsight 3.8.0, Apache htpasswd 2.4, Apache httpd htpasswd source 2.4.69, and Moby port mapping source v28.3.3; 2 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- private (REASONED; grafana, prometheus, phpmyadmin, pgadmin, adminer, redisinsight): Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice. -- mongo-enable (REASONED; mongo): Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify. -- mongo-credentials (REASONED; mongo): Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults. -- mongo-database (REASONED; mongo): Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL. -- grafana-admin (REASONED; grafana): First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts. -- grafana-anonymous (REASONED; grafana): Disable Grafana anonymous access if enabled. -- grafana-mfa (REASONED; grafana): Prefer SSO with MFA at the identity provider. -- grafana-bind (REASONED; grafana-pin, go): Empty http_addr joins the port and binds all interfaces; set 127.0.0.1. -- grafana-tls (REASONED; grafana): Native HTTPS uses server protocol=https, cert_file and cert_key. -- prometheus-auth (REASONED; prometheus): Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes. -- prometheus-bcrypt (REASONED; prometheus, htpasswd, httpd-source, owasp-source): Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt. -- prometheus-bind (REASONED; prometheus-pin): Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090. -- prometheus-tls (REASONED; prometheus): web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config. -- database-panels (REASONED; phpmyadmin, pgadmin): Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated. -- pgadmin-login (REASONED; pgadmin): pgAdmin server mode has its own accounts/login. -- adminer-login (REASONED; adminer): Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts. -- adminer-file (REASONED; adminer): Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies. -- verify-listeners (REASONED; grafana-pin, prometheus-pin, engine-pinned, moby-pinned): Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy. -- verify-login (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive. -- verify-data (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are 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. Database and monitoring panels are the most-scanned targets on the internet, and several ship with known default credentials. One rule dominates everything tool-specific below: **an admin panel is never reachable from the public internet.** Bind it to loopback and reach it through SSH port forwarding, a VPN or tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) plus MFA ([mfa.md](mfa.md)). @@ -309,3 +114,224 @@ REASONED: preceding block; listener inventory, anonymous front-page/data-route c - Docker conditional 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 Linux firewall rules for publication, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 - Moby port mapping at v28.3.3, conditional proxy startup and TCP listening without a proxy (checked October 2026): https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.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/admin-uis.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): + +- phpmyadmin: phpMyAdmin (unknown) +- grafana-pin: Grafana (v13.2.2) +- prometheus-pin: Prometheus (v3.14.0) +- pgadmin: pgAdmin (unknown) +- mongo: mongo-express (unknown) +- grafana: Grafana docs (unknown) +- prometheus: Prometheus docs (unknown) +- adminer: Adminer (unknown) +- redisinsight: RedisInsight (3.8.0) +- go: Go net (unknown) +- htpasswd: Apache htpasswd (2.4) +- httpd-source: Apache httpd htpasswd source (2.4.69) +- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- moby-pinned: Moby port mapping source (v28.3.3) +- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; grafana, prometheus, phpmyadmin, pgadmin, adminer, redisinsight): Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice. +- mongo-enable (REASONED; mongo): Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify. +- mongo-credentials (REASONED; mongo): Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults. +- mongo-database (REASONED; mongo): Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL. +- grafana-admin (REASONED; grafana): First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts. +- grafana-anonymous (REASONED; grafana): Disable Grafana anonymous access if enabled. +- grafana-mfa (REASONED; grafana): Prefer SSO with MFA at the identity provider. +- grafana-bind (REASONED; grafana-pin, go): Empty http_addr joins the port and binds all interfaces; set 127.0.0.1. +- grafana-tls (REASONED; grafana): Native HTTPS uses server protocol=https, cert_file and cert_key. +- prometheus-auth (REASONED; prometheus): Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes. +- prometheus-bcrypt (REASONED; prometheus, htpasswd, httpd-source, owasp-source): Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt. +- prometheus-bind (REASONED; prometheus-pin): Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090. +- prometheus-tls (REASONED; prometheus): web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config. +- database-panels (REASONED; phpmyadmin, pgadmin): Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated. +- pgadmin-login (REASONED; pgadmin): pgAdmin server mode has its own accounts/login. +- adminer-login (REASONED; adminer): Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts. +- adminer-file (REASONED; adminer): Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies. +- verify-listeners (REASONED; grafana-pin, prometheus-pin, engine-pinned, moby-pinned): Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy. +- verify-login (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive. +- verify-data (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "92d3d0f412f545709088bb0ce3d9b82c95988187a6fd4fcf75747d818f8d5c8b", + "components": { + "phpmyadmin": { + "name": "phpMyAdmin", + "basis": "unknown", + "sources": { + "s5f6c8965c539": "https://www.phpmyadmin.net/docs/" + } + }, + "grafana-pin": { + "name": "Grafana", + "basis": "v13.2.2", + "sources": { + "s4c6ff3174e7e": "https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50", + "sb00e234d0ddd": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469", + "sf14b29976adb": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568" + } + }, + "prometheus-pin": { + "name": "Prometheus", + "basis": "v3.14.0", + "sources": { + "s6d5f308b02a2": "https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425" + } + }, + "pgadmin": { + "name": "pgAdmin", + "basis": "unknown", + "sources": { + "s2f7e33714435": "https://www.pgadmin.org/docs/" + } + }, + "mongo": { + "name": "mongo-express", + "basis": "unknown", + "sources": { + "s69ec4eee2c85": "https://github.com/mongo-express/mongo-express" + } + }, + "grafana": { + "name": "Grafana docs", + "basis": "unknown", + "sources": { + "s7aa447405cc5": "https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/", + "sc241e0772095": "https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/" + } + }, + "prometheus": { + "name": "Prometheus docs", + "basis": "unknown", + "sources": { + "scce074c47e9c": "https://prometheus.io/docs/guides/basic-auth/", + "sc48b506cc29a": "https://prometheus.io/docs/guides/tls-encryption/" + } + }, + "adminer": { + "name": "Adminer", + "basis": "unknown", + "sources": { + "sb5eda9397b18": "https://www.adminer.org/" + } + }, + "redisinsight": { + "name": "RedisInsight", + "basis": "3.8.0", + "sources": { + "s6b8fb1c9bbbc": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100", + "s7ba57be16bb3": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133", + "sddf9b5ee80b9": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49", + "s5e6e219b4b65": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35" + } + }, + "go": { + "name": "Go net", + "basis": "unknown", + "sources": { + "s37204ff1b27c": "https://pkg.go.dev/net#Listen" + } + }, + "htpasswd": { + "name": "Apache htpasswd", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "httpd-source": { + "name": "Apache httpd htpasswd source", + "basis": "2.4.69", + "sources": { + "sfed14ba2991a": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297", + "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", + "s4ec6b9b216dc": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383", + "s6952d96564d9": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129" + } + }, + "engine-pinned": { + "name": "Docker Engine networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sb3dacfbaf7e1": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38", + "s1c82ff316a7e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62", + "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" + } + }, + "moby-pinned": { + "name": "Moby port mapping source", + "basis": "v28.3.3", + "sources": { + "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" + } + }, + "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": { + "private": {"text": "Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice.", "components": ["grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer", "redisinsight"], "sources": ["grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18", "redisinsight:s6b8fb1c9bbbc", "redisinsight:s7ba57be16bb3", "redisinsight:sddf9b5ee80b9", "redisinsight:s5e6e219b4b65"], "status": "REASONED"}, + "mongo-enable": {"text": "Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "mongo-credentials": {"text": "Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "mongo-database": {"text": "Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "grafana-admin": {"text": "First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-anonymous": {"text": "Disable Grafana anonymous access if enabled.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-mfa": {"text": "Prefer SSO with MFA at the identity provider.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-bind": {"text": "Empty http_addr joins the port and binds all interfaces; set 127.0.0.1.", "components": ["grafana-pin", "go"], "sources": ["grafana-pin:s4c6ff3174e7e", "grafana-pin:sb00e234d0ddd", "grafana-pin:sf14b29976adb", "go:s37204ff1b27c"], "status": "REASONED"}, + "grafana-tls": {"text": "Native HTTPS uses server protocol=https, cert_file and cert_key.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5", "grafana:sc241e0772095"], "status": "REASONED"}, + "prometheus-auth": {"text": "Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes.", "components": ["prometheus"], "sources": ["prometheus:scce074c47e9c"], "status": "REASONED"}, + "prometheus-bcrypt": {"text": "Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt.", "components": ["prometheus", "htpasswd", "httpd-source", "owasp-source"], "sources": ["prometheus:scce074c47e9c", "htpasswd:scde9004bb967", "httpd-source:sfed14ba2991a", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4ec6b9b216dc", "httpd-source:s6952d96564d9", "owasp-source:s4531e65b6502"], "status": "REASONED"}, + "prometheus-bind": {"text": "Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090.", "components": ["prometheus-pin"], "sources": ["prometheus-pin:s6d5f308b02a2"], "status": "REASONED"}, + "prometheus-tls": {"text": "web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config.", "components": ["prometheus"], "sources": ["prometheus:sc48b506cc29a"], "status": "REASONED"}, + "database-panels": {"text": "Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated.", "components": ["phpmyadmin", "pgadmin"], "sources": ["phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435"], "status": "REASONED"}, + "pgadmin-login": {"text": "pgAdmin server mode has its own accounts/login.", "components": ["pgadmin"], "sources": ["pgadmin:s2f7e33714435"], "status": "REASONED"}, + "adminer-login": {"text": "Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, + "adminer-file": {"text": "Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy.", "components": ["grafana-pin", "prometheus-pin", "engine-pinned", "moby-pinned"], "sources": ["grafana-pin:s4c6ff3174e7e", "prometheus-pin:s6d5f308b02a2", "engine-pinned:sb3dacfbaf7e1", "engine-pinned:s1c82ff316a7e", "engine-pinned:sc244607b20e4", "moby-pinned:seaf4197aafaa"], "status": "REASONED", "verify": [1]}, + "verify-login": {"text": "Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]}, + "verify-data": {"text": "A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/bi-dashboards.md b/bi-dashboards.md index 3b6ba5094..47e1be786 100644 --- a/bi-dashboards.md +++ b/bi-dashboards.md @@ -1,175 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "72fb83f126c17a7bf6712bea8620686191699a388b522b41778ca65b8b8c506f", - "components": { - "metabase-docs": { - "name": "Metabase docs", - "basis": "unknown", - "sources": { - "sefe12b1b495c": "https://www.metabase.com/docs/latest/", - "s5e2251b3184d": "https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase", - "s521b3063e9f0": "https://www.metabase.com/docs/latest/embedding/public-links", - "s02b7797b6ffe": "https://www.metabase.com/docs/latest/permissions/data", - "s434abdb1aaef": "https://www.metabase.com/docs/latest/databases/users-roles-privileges" - } - }, - "metabase": { - "name": "Metabase", - "basis": "v0.63.18", - "sources": { - "s8df4cbf03036": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L54", - "s7dbf92797642": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L76-L88", - "s7e71ce4cff1b": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/server/instance.clj#L36-L41", - "sed761829c85c": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L79", - "sea59c4ef7f2e": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L164", - "sabe1aa2cf0a9": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L195", - "s18f6f2188fe9": "https://github.com/metabase/metabase/blob/v0.63.18/bin/docker/run_metabase.sh#L2-L5", - "s9b0c73c666ff": "https://github.com/metabase/metabase/blob/v0.63.18/Dockerfile#L66-L69" - } - }, - "environ": { - "name": "environ", - "basis": "1.2.0", - "sources": { - "s37ed6de2d80d": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L32-L43", - "s9754f7c47384": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L64-L79" - } - }, - "ring": { - "name": "Ring Jetty adapter", - "basis": "1.15.3", - "sources": { - "s0455b1b1fb12": "https://github.com/ring-clojure/ring/blob/1.15.3/ring-jetty-adapter/src/ring/adapter/jetty.clj#L202-L207" - } - }, - "jetty": { - "name": "Jetty", - "basis": "12.1.13", - "sources": { - "s8f9e4989175e": "https://github.com/jetty/jetty.project/blob/jetty-12.1.13/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java#L338-L344" - } - }, - "superset": { - "name": "Superset docs", - "basis": "unknown", - "sources": { - "se97be7353585": "https://superset.apache.org/admin-docs/security/" - } - }, - "superset-compose": { - "name": "Superset Compose", - "basis": "3d01094d231712e3470eb625be3f9925aeaeadb5", - "sources": { - "s0ceca7af7f79": "https://github.com/apache/superset/blob/3d01094d231712e3470eb625be3f9925aeaeadb5/docker-compose.yml" - } - }, - "redash-docs": { - "name": "Redash docs", - "basis": "unknown", - "sources": { - "s069a74340b65": "https://redash.io/help/", - "s4c72252b5447": "https://redash.io/help/open-source/setup/", - "sbaf5b44079a0": "https://redash.io/help/open-source/admin-guide/secrets/" - } - }, - "redash": { - "name": "Redash", - "basis": "v26.9.0", - "sources": { - "se93d7c47b037": "https://github.com/getredash/redash/blob/v26.9.0/bin/docker-entrypoint#L49-L50", - "s9f1ef7237804": "https://github.com/getredash/redash/blob/v26.9.0/Dockerfile#L133-L134" - } - }, - "docker": { - "name": "Docker publishing", - "basis": "unknown", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - } - }, - "claims": { - "private": {"text": "Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed.", "components": ["redash-docs", "metabase-docs"], "sources": ["redash-docs:s4c72252b5447", "metabase-docs:s434abdb1aaef"], "status": "REASONED"}, - "metabase-setup": {"text": "First account is admin; finish setup before anyone else can reach it.", "components": ["metabase-docs"], "sources": ["metabase-docs:s5e2251b3184d"], "status": "REASONED"}, - "metabase-precedence": {"text": "Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings.", "components": ["metabase", "environ"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "environ:s37ed6de2d80d", "environ:s9754f7c47384"], "status": "REASONED"}, - "metabase-bind": {"text": "Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy.", "components": ["metabase", "ring", "jetty"], "sources": ["metabase:s7e71ce4cff1b", "metabase:sed761829c85c", "metabase:sea59c4ef7f2e", "metabase:sabe1aa2cf0a9", "ring:s0455b1b1fb12", "jetty:s8f9e4989175e"], "status": "REASONED"}, - "metabase-image": {"text": "Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start.", "components": ["metabase", "docker"], "sources": ["metabase:s18f6f2188fe9", "metabase:s9b0c73c666ff", "docker:s1e53417c513d"], "status": "REASONED"}, - "metabase-port": {"text": "Port defaults 3000 unless the final MB_JETTY_PORT is nonblank.", "components": ["metabase"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "metabase:s7e71ce4cff1b"], "status": "REASONED"}, - "metabase-sharing": {"text": "Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, - "metabase-embed": {"text": "Public embed URLs are recoverable; embeds do not strengthen the public-link boundary.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, - "metabase-sandbox": {"text": "Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries.", "components": ["metabase-docs"], "sources": ["metabase-docs:s02b7797b6ffe"], "status": "REASONED"}, - "superset-secret": {"text": "Set a unique complex random SECRET_KEY before exposure; it signs session cookies.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, - "superset-admin": {"text": "Replace the first-run admin password before exposure.", "components": ["superset", "superset-compose"], "sources": ["superset:se97be7353585", "superset-compose:s0ceca7af7f79"], "status": "REASONED"}, - "superset-public": {"text": "Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, - "superset-copy": {"text": "PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, - "superset-compose": {"text": "Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data.", "components": ["superset-compose"], "sources": ["superset-compose:s0ceca7af7f79"], "status": "REASONED"}, - "redash-setup": {"text": "Create the first-run admin before others reach setup.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, - "redash-https": {"text": "Enforce HTTPS and the private-access pattern.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, - "redash-cookie": {"text": "Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, - "redash-encryption": {"text": "REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, - "redash-bind": {"text": "Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately.", "components": ["redash"], "sources": ["redash:se93d7c47b037", "redash:s9f1ef7237804"], "status": "REASONED"}, - "verify-credentials": {"text": "Confirm no default admin/admin or example credentials work.", "components": ["metabase-docs", "superset-compose", "redash-docs"], "sources": ["metabase-docs:s5e2251b3184d", "superset-compose:s0ceca7af7f79", "redash-docs:s4c72252b5447"], "status": "REASONED"}, - "verify-db": {"text": "Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately.", "components": ["metabase-docs"], "sources": ["metabase-docs:s434abdb1aaef"], "status": "REASONED"}, - "verify-listeners": {"text": "Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation.", "components": ["metabase", "superset-compose", "redash", "docker"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]}, - "verify-ports": {"text": "Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent.", "components": ["metabase", "superset-compose", "redash"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037"], "status": "REASONED", "verify": [1]}, - "verify-data": {"text": "Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive.", "components": ["metabase-docs", "superset", "redash-docs"], "sources": ["metabase-docs:s521b3063e9f0", "superset:se97be7353585", "redash-docs:s4c72252b5447"], "status": "REASONED", "verify": [1]}, - "verify-private": {"text": "For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path.", "components": ["redash-docs", "docker"], "sources": ["redash-docs:s4c72252b5447", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]} - } -} ---- # BI dashboards: Metabase, Superset, Redash - -**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): - -- metabase-docs: Metabase docs (unknown) -- metabase: Metabase (v0.63.18) -- environ: environ (1.2.0) -- ring: Ring Jetty adapter (1.15.3) -- jetty: Jetty (12.1.13) -- superset: Superset docs (unknown) -- superset-compose: Superset Compose (3d01094d231712e3470eb625be3f9925aeaeadb5) -- redash-docs: Redash docs (unknown) -- redash: Redash (v26.9.0) -- docker: Docker publishing (unknown) - -Claims (full source mapping in the front matter): +Written against Metabase v0.63.18, Redash v26.9.0, environ 1.2.0, Ring Jetty adapter 1.15.3, and Jetty 12.1.13; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 25 claims, all reasoned from cited sources. -- private (REASONED; redash-docs, metabase-docs): Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed. -- metabase-setup (REASONED; metabase-docs): First account is admin; finish setup before anyone else can reach it. -- metabase-precedence (REASONED; metabase, environ): Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings. -- metabase-bind (REASONED; metabase, ring, jetty): Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy. -- metabase-image (REASONED; metabase, docker): Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start. -- metabase-port (REASONED; metabase): Port defaults 3000 unless the final MB_JETTY_PORT is nonblank. -- metabase-sharing (REASONED; metabase-docs): Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing. -- metabase-embed (REASONED; metabase-docs): Public embed URLs are recoverable; embeds do not strengthen the public-link boundary. -- metabase-sandbox (REASONED; metabase-docs): Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries. -- superset-secret (REASONED; superset): Set a unique complex random SECRET_KEY before exposure; it signs session cookies. -- superset-admin (REASONED; superset, superset-compose): Replace the first-run admin password before exposure. -- superset-public (REASONED; superset): Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default. -- superset-copy (REASONED; superset): PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards. -- superset-compose (REASONED; superset-compose): Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data. -- redash-setup (REASONED; redash-docs): Create the first-run admin before others reach setup. -- redash-https (REASONED; redash-docs): Enforce HTTPS and the private-access pattern. -- redash-cookie (REASONED; redash-docs): Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it. -- redash-encryption (REASONED; redash-docs): REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control. -- redash-bind (REASONED; redash): Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately. -- verify-credentials (REASONED; metabase-docs, superset-compose, redash-docs): Confirm no default admin/admin or example credentials work. -- verify-db (REASONED; metabase-docs): Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately. -- verify-listeners (REASONED; metabase, superset-compose, redash, docker): Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation. -- verify-ports (REASONED; metabase, superset-compose, redash): Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent. -- verify-data (REASONED; metabase-docs, superset, redash-docs): Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive. -- verify-private (REASONED; redash-docs, docker): For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path. - +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 tools hold live connections to your production databases and cache query results in their own storage. An exposed instance, or one still running default or example credentials, leaks both the @@ -296,3 +129,196 @@ connection string or the tool's own login is not enough. - Redash secret keys (cookie signing, data-source-credential encryption, do not reuse across instances or commit): https://redash.io/help/open-source/admin-guide/secrets/ - Metabase database users, roles, and privileges (a dedicated least-privilege read-only account): https://www.metabase.com/docs/latest/databases/users-roles-privileges - Docker port publishing (a published container port is served through NAT/forwarding rules, so the host may show no matching listener): https://docs.docker.com/engine/network/port-publishing/ + + + +## 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/bi-dashboards.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): + +- metabase: Metabase (v0.63.18) +- redash: Redash (v26.9.0) +- metabase-docs: Metabase docs (unknown) +- superset: Superset docs (unknown) +- superset-compose: Superset Compose (3d01094d231712e3470eb625be3f9925aeaeadb5) +- redash-docs: Redash docs (unknown) +- environ: environ (1.2.0) +- ring: Ring Jetty adapter (1.15.3) +- jetty: Jetty (12.1.13) +- docker: Docker publishing (unknown) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; redash-docs, metabase-docs): Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed. +- metabase-setup (REASONED; metabase-docs): First account is admin; finish setup before anyone else can reach it. +- metabase-precedence (REASONED; metabase, environ): Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings. +- metabase-bind (REASONED; metabase, ring, jetty): Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy. +- metabase-image (REASONED; metabase, docker): Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start. +- metabase-port (REASONED; metabase): Port defaults 3000 unless the final MB_JETTY_PORT is nonblank. +- metabase-sharing (REASONED; metabase-docs): Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing. +- metabase-embed (REASONED; metabase-docs): Public embed URLs are recoverable; embeds do not strengthen the public-link boundary. +- metabase-sandbox (REASONED; metabase-docs): Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries. +- superset-secret (REASONED; superset): Set a unique complex random SECRET_KEY before exposure; it signs session cookies. +- superset-admin (REASONED; superset, superset-compose): Replace the first-run admin password before exposure. +- superset-public (REASONED; superset): Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default. +- superset-copy (REASONED; superset): PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards. +- superset-compose (REASONED; superset-compose): Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data. +- redash-setup (REASONED; redash-docs): Create the first-run admin before others reach setup. +- redash-https (REASONED; redash-docs): Enforce HTTPS and the private-access pattern. +- redash-cookie (REASONED; redash-docs): Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it. +- redash-encryption (REASONED; redash-docs): REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control. +- redash-bind (REASONED; redash): Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately. +- verify-credentials (REASONED; metabase-docs, superset-compose, redash-docs): Confirm no default admin/admin or example credentials work. +- verify-db (REASONED; metabase-docs): Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately. +- verify-listeners (REASONED; metabase, superset-compose, redash, docker): Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation. +- verify-ports (REASONED; metabase, superset-compose, redash): Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent. +- verify-data (REASONED; metabase-docs, superset, redash-docs): Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive. +- verify-private (REASONED; redash-docs, docker): For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "72fb83f126c17a7bf6712bea8620686191699a388b522b41778ca65b8b8c506f", + "components": { + "metabase": { + "name": "Metabase", + "basis": "v0.63.18", + "sources": { + "s8df4cbf03036": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L54", + "s7dbf92797642": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L76-L88", + "s7e71ce4cff1b": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/server/instance.clj#L36-L41", + "sed761829c85c": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L79", + "sea59c4ef7f2e": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L164", + "sabe1aa2cf0a9": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L195", + "s18f6f2188fe9": "https://github.com/metabase/metabase/blob/v0.63.18/bin/docker/run_metabase.sh#L2-L5", + "s9b0c73c666ff": "https://github.com/metabase/metabase/blob/v0.63.18/Dockerfile#L66-L69" + } + }, + "redash": { + "name": "Redash", + "basis": "v26.9.0", + "sources": { + "se93d7c47b037": "https://github.com/getredash/redash/blob/v26.9.0/bin/docker-entrypoint#L49-L50", + "s9f1ef7237804": "https://github.com/getredash/redash/blob/v26.9.0/Dockerfile#L133-L134" + } + }, + "metabase-docs": { + "name": "Metabase docs", + "basis": "unknown", + "sources": { + "sefe12b1b495c": "https://www.metabase.com/docs/latest/", + "s5e2251b3184d": "https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase", + "s521b3063e9f0": "https://www.metabase.com/docs/latest/embedding/public-links", + "s02b7797b6ffe": "https://www.metabase.com/docs/latest/permissions/data", + "s434abdb1aaef": "https://www.metabase.com/docs/latest/databases/users-roles-privileges" + } + }, + "superset": { + "name": "Superset docs", + "basis": "unknown", + "sources": { + "se97be7353585": "https://superset.apache.org/admin-docs/security/" + } + }, + "superset-compose": { + "name": "Superset Compose", + "basis": "3d01094d231712e3470eb625be3f9925aeaeadb5", + "sources": { + "s0ceca7af7f79": "https://github.com/apache/superset/blob/3d01094d231712e3470eb625be3f9925aeaeadb5/docker-compose.yml" + } + }, + "redash-docs": { + "name": "Redash docs", + "basis": "unknown", + "sources": { + "s069a74340b65": "https://redash.io/help/", + "s4c72252b5447": "https://redash.io/help/open-source/setup/", + "sbaf5b44079a0": "https://redash.io/help/open-source/admin-guide/secrets/" + } + }, + "environ": { + "name": "environ", + "basis": "1.2.0", + "sources": { + "s37ed6de2d80d": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L32-L43", + "s9754f7c47384": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L64-L79" + } + }, + "ring": { + "name": "Ring Jetty adapter", + "basis": "1.15.3", + "sources": { + "s0455b1b1fb12": "https://github.com/ring-clojure/ring/blob/1.15.3/ring-jetty-adapter/src/ring/adapter/jetty.clj#L202-L207" + } + }, + "jetty": { + "name": "Jetty", + "basis": "12.1.13", + "sources": { + "s8f9e4989175e": "https://github.com/jetty/jetty.project/blob/jetty-12.1.13/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java#L338-L344" + } + }, + "docker": { + "name": "Docker publishing", + "basis": "unknown", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + } + }, + "claims": { + "private": {"text": "Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed.", "components": ["redash-docs", "metabase-docs"], "sources": ["redash-docs:s4c72252b5447", "metabase-docs:s434abdb1aaef"], "status": "REASONED"}, + "metabase-setup": {"text": "First account is admin; finish setup before anyone else can reach it.", "components": ["metabase-docs"], "sources": ["metabase-docs:s5e2251b3184d"], "status": "REASONED"}, + "metabase-precedence": {"text": "Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings.", "components": ["metabase", "environ"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "environ:s37ed6de2d80d", "environ:s9754f7c47384"], "status": "REASONED"}, + "metabase-bind": {"text": "Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy.", "components": ["metabase", "ring", "jetty"], "sources": ["metabase:s7e71ce4cff1b", "metabase:sed761829c85c", "metabase:sea59c4ef7f2e", "metabase:sabe1aa2cf0a9", "ring:s0455b1b1fb12", "jetty:s8f9e4989175e"], "status": "REASONED"}, + "metabase-image": {"text": "Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start.", "components": ["metabase", "docker"], "sources": ["metabase:s18f6f2188fe9", "metabase:s9b0c73c666ff", "docker:s1e53417c513d"], "status": "REASONED"}, + "metabase-port": {"text": "Port defaults 3000 unless the final MB_JETTY_PORT is nonblank.", "components": ["metabase"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "metabase:s7e71ce4cff1b"], "status": "REASONED"}, + "metabase-sharing": {"text": "Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, + "metabase-embed": {"text": "Public embed URLs are recoverable; embeds do not strengthen the public-link boundary.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, + "metabase-sandbox": {"text": "Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries.", "components": ["metabase-docs"], "sources": ["metabase-docs:s02b7797b6ffe"], "status": "REASONED"}, + "superset-secret": {"text": "Set a unique complex random SECRET_KEY before exposure; it signs session cookies.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-admin": {"text": "Replace the first-run admin password before exposure.", "components": ["superset", "superset-compose"], "sources": ["superset:se97be7353585", "superset-compose:s0ceca7af7f79"], "status": "REASONED"}, + "superset-public": {"text": "Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-copy": {"text": "PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-compose": {"text": "Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data.", "components": ["superset-compose"], "sources": ["superset-compose:s0ceca7af7f79"], "status": "REASONED"}, + "redash-setup": {"text": "Create the first-run admin before others reach setup.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, + "redash-https": {"text": "Enforce HTTPS and the private-access pattern.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, + "redash-cookie": {"text": "Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, + "redash-encryption": {"text": "REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, + "redash-bind": {"text": "Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately.", "components": ["redash"], "sources": ["redash:se93d7c47b037", "redash:s9f1ef7237804"], "status": "REASONED"}, + "verify-credentials": {"text": "Confirm no default admin/admin or example credentials work.", "components": ["metabase-docs", "superset-compose", "redash-docs"], "sources": ["metabase-docs:s5e2251b3184d", "superset-compose:s0ceca7af7f79", "redash-docs:s4c72252b5447"], "status": "REASONED"}, + "verify-db": {"text": "Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately.", "components": ["metabase-docs"], "sources": ["metabase-docs:s434abdb1aaef"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation.", "components": ["metabase", "superset-compose", "redash", "docker"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]}, + "verify-ports": {"text": "Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent.", "components": ["metabase", "superset-compose", "redash"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037"], "status": "REASONED", "verify": [1]}, + "verify-data": {"text": "Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive.", "components": ["metabase-docs", "superset", "redash-docs"], "sources": ["metabase-docs:s521b3063e9f0", "superset:se97be7353585", "redash-docs:s4c72252b5447"], "status": "REASONED", "verify": [1]}, + "verify-private": {"text": "For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path.", "components": ["redash-docs", "docker"], "sources": ["redash-docs:s4c72252b5447", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/deployment-lifecycle.md b/deployment-lifecycle.md index f741e99d7..fbc13453a 100644 --- a/deployment-lifecycle.md +++ b/deployment-lifecycle.md @@ -1,5 +1,235 @@ ---- -version_basis: { +# Deploying safely over time: verify from outside, safe order, previews, teardown + +Written against Nmap documentation (no recorded version), BIND dig documentation 9.18.39, OpenSSL command documentation 3.0, OpenBSD nc manual 7.6, OWASP Web Security Testing Guide v4.2, OAuth2 Proxy v7.15.4, Authelia v4.39.28, and NIST digital identity guidelines SP 800-63B-4, plus 2 more recorded bases; 9 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 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. + +This repository's per-service guides check TLS, binding, and authentication from inside the host and at the moment you set them up. Real exposure is judged from outside the host, and it drifts: a preview URL, a restored backup, or an upgrade can reopen something already closed. This guide is the lifecycle layer around the per-service checks. + +## 1. Verify from outside, not just from the host + +A `curl` or `ss -tlnp` run on the host itself cannot see a firewall the host does not know about, or a platform-level bypass like Docker's iptables rules ([docker.md](docker.md)). Verify from a second network: a phone hotspot, a cloud shell, or a second VM. + +- Port-by-port: `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 REPLACE_WITH_YOUR_PUBLIC_IP "$p"; done`, or a full sweep, `nmap -Pn -p- REPLACE_WITH_YOUR_PUBLIC_IP` (per the nmap reference, which documents `-p-` and `-p 1-65535` as equivalent port-range syntax; this sweeps TCP only, so inventory and separately probe any UDP service with `nmap -sU` on its ports; only scan hosts you own or are authorized to test). Before running either form, use the [guarded Verify block](#verify); for the loop, replace only its IPv4 arm's `nmap -Pn -p- "$1"` with `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 "$1" "$p"; done`, keeping the whole guard and substituting inside the quotes on `set --`. Ubuntu noble netcat-openbsd documents `-v` as verbose output, `-z` as scanning without sending data and `-w` as a connection/idle timeout. BusyBox 1.36.1 small `nc` lacks `-v` and `-z`, and has `-w` only with `NC_EXTRA`; its `NC_110_COMPAT` applet lists `-v` and `-w`, with `-z` conditional on `NC_EXTRA`. CentOS Stream 9 packages Ncat 7.92 as an `nc` alternative; Ncat 7.92's option behaviour is not established here, nor is traditional or GNU netcat behaviour. Check the installed variant. As this guide's assessment, refusal or timeout shows only that this connection failed; an unsupported-option error, local error or silent exit is inconclusive. +- Check every address the service actually has, not just the one you remember configuring: the public IPv4 address, the public IPv6 address if the host has one, and any platform-assigned URL alongside your custom domain (a PaaS default subdomain, per [paas.md](paas.md), often stays reachable even when the custom domain is fronted). A scan of one address that misses the others is not a clean result, it is an incomplete one. +- Cross-reference what is already indexed about your IP with a passive internet-wide scanner such as Shodan or Censys; both build a continuously updated index of internet-connected hosts and services, so a stale exposure can show up there before you find it yourself ([cloud-firewalls.md](cloud-firewalls.md) covers the firewall rules this is checking). + +## 2. Safe initialization order + +This guide advises the following first-deployment order, keeping application access closed until initialization and the fronting gate are ready: + +1. Create the owner or admin account's credentials and any signing secrets (session, JWT, or cookie-signing keys) per [authentication.md](authentication.md); the ordering is this guide's advice, and signing secrets follow that guide's recommendation. +2. Set the registration policy (disable open self-registration, or restrict it to an allowlisted domain) before opening application access, as this guide advises; [authentication.md](authentication.md) covers allowlisting after federated login. +3. Put the fronting gate in place, TLS and any proxy-level authentication, per [free-certificates.md](free-certificates.md) and the fronting-layer guides. For ACME, this guide advises DNS-01 issuance before opening inbound access, or opening only the challenge port during issuance while keeping application access blocked: HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443, whereas DNS-01 needs no inbound port, per the [prerequisite list](free-certificates.md#prerequisites). These challenge requirements do not establish this deployment order. Cloudflare recommends creating the Access application before the tunnel route and warns that without it the published application is available to anyone on the Internet; an already publicly routable origin needs its IP protected by other methods, per the pinned self-hosted application instructions. +4. Only then open application network access, as this guide advises. + +For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin, open to whoever completes it first. [Open WebUI](open-webui.md) at commit 0a7c15832fb30b1903753e83f81dc7d27e5b0944 is one example: its signup handler makes the first account admin; with authentication enabled, ENABLE_SIGNUP and ENABLE_LOGIN_FORM default to true, and an empty database bypasses ENABLE_SIGNUP when the login form is enabled. Once accounts exist, disabled signup refuses registration. These Open WebUI paths do not establish other applications' bootstrap, signing-secret, registration or domain-enforcement behaviour. After initialization, confirm the setup route itself is closed: restart the service and try hitting the initial-setup URL again; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). The Open WebUI signup handler illustrates bootstrap and signup refusal, not every application's setup route or a deployed restart result. + +## 3. Previews and clones get production posture + +A preview deployment or a database clone is not lower stakes just because it is temporary. Vercel's Deployment Protection illustrates the gap: Standard Protection, available on every plan, gates preview and generated deployment URLs but leaves the production domain open by default; the All Deployments scope closes that gap too, and Vercel's September 9, 2026 change made pairing it with Vercel Authentication free on every plan rather than Pro and Enterprise only ([paas.md](paas.md); per Vercel's Deployment Protection changelog of 9 September 2026 and its current Deployment Protection reference, which states that Vercel Authentication for All Deployments does not require a paid add-on). Where the platform's own gate does not cover a hostname, front it the same way as production, for example a Cloudflare Access policy scoped to that preview hostname, per the pinned self-hosted application instructions. Those instructions establish the Access gate, not the platform's coverage of preview URLs. Either way, as this guide's advice, treat a clone's data the same as the original: rotate any credential a clone inherited if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. + +## 4. Teardown: DNS before the app, then revoke the rest + +Retiring a deployment in the wrong order can leave a dangling DNS record pointing at a resource someone else can claim (a subdomain takeover); OWASP's Subdomain Takeover Prevention Cheat Sheet describes this mechanism, and WSTG v4.2 identifies an inactive target and inadequate ownership verification as preconditions. Delete the DNS record (the `CNAME` or `A`/`AAAA`) before you delete or release the underlying app, load balancer, or IP, and wait at least the record's old TTL before decommissioning the resource, following OWASP's order; WSTG supports removing vulnerable records but does not specify that wait. + +RFC 1034 section 3.6 and RFC 1035 section 3.2.1 describe resolver caching and TTL, with TTL bounds clarified by RFC 2181 section 8 and updated by RFC 8767 section 4. **TTL expiry is not a hard bound: RFC 8767 permits serving stale records when they cannot be authoritatively refreshed.** This guide infers that deleting a record does not flush cached answers, so immediately releasing a reclaimable IP or hostname can still send cached clients to its next owner. OWASP describes claimable CNAME targets and released IPs that an attacker obtains after reassignment; the cited files do not establish a particular platform's current reclaimability. This guide advises keeping the resource reserved through the old TTL and until removal is confirmed at every authoritative nameserver, and accounting for serve-stale before release; authoritative removal and elapsed TTL do not prove that every resolver has stopped returning the old mapping. + +Then revoke what pointed at it: access policies (Cloudflare Access or equivalent), API tokens and service credentials scoped to that deployment ([machine-auth.md](machine-auth.md)), and database users created only for it. This remains the guide's retirement requirement: OWASP Secrets Management requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only, and the cited files do not establish removal of an Access application or policy or dropping database users. + +## 5. Re-verify after anything changes + +Rerun the outside-in checks in section 1 and the negative auth tests from [authentication.md](authentication.md) after an upgrade, a backup restore (which, as this guide's assessment, can reintroduce a default account or reset a feature flag), or any change to network policy or auth configuration. Docker's pinned 28.x release notes give concrete change-related cases: 28.0.0 sets IPv6 `FORWARD` policy to `DROP` only when Docker enables forwarding itself, warning that hosts with forwarding already enabled that relied on Docker setting `DROP` may need configuration changes; 28.3.3 fixes direct local-network access to published container ports after a firewalld reload, even when intended for loopback-only access. Open WebUI's pinned configuration supplies an environment-derived signup default and enables persistence by default. As this guide's conditional inference for restores, if database values override the environment after first launch as described in [open-webui.md](open-webui.md), restoring earlier database values could reinstate earlier signup settings; the configuration this guide cites delegates to `models.config.Config`, whose implementation this guide does not cite, so these files do not establish that precedence or general restore behaviour. Nmap supports scanning, not application restore behaviour. Monitor certificate expiry from outside the host too: as this guide's assessment, a renewal cron can silently fail while the host's own view still looks fine, so this guide's advice is an external check (a scheduled `openssl s_client` from another machine, or a third-party uptime/certificate monitor). Certbot v2.3.0's `certbot renew --dry-run` uses the staging test server unless a custom server is set, obtains test certificates without saving them and does not run deploy hooks unless `--run-deploy-hooks` is supplied; applicable deploy hooks then run only after a successful dry run, using the current active certificate. The option documentation also warns that dry runs can temporarily change webserver configuration, reload webservers and run pre/post hooks; the user guide's dry-run example distinguishes it from a subsequent live renewal. From these documented limits, this guide infers that a dry run cannot show whether the last real renewal succeeded; Nmap is outside-check context only. + +## 6. Offboarding + +When a person leaves, revoke access at every layer they touched, not only their password: identity-provider membership (the SSO or directory account), proxy-level sessions (Cloudflare Access or equivalent), application-level sessions on each service, and any personal access tokens or API keys issued to them (this guide's recommendation, as in [authentication.md](authentication.md)). + +Microsoft Entra documents blocking new sign-ins and revoking refresh tokens, but the application must revoke its own session; access-token lifetimes vary, and CAE critical-event enforcement can take up to 15 minutes for supported services. Cloudflare Access documents per-user session revocation, but an active IdP profile can start a new session; after user logout, previously issued tokens stop being accepted in 20-30 seconds. OAuth2 Proxy v7.15.4 documents cookie expiry and refresh, and Authelia v4.39.28 documents cookie expiry and session destruction, overridden by remember-me; these passages do not establish immediate per-user revocation for either proxy. RFC 7009 section 2.1 specifies immediate token invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST SP 800-63B-4 section 4.5 requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. + +Where a system cannot revoke immediately, such as a session cached until its own expiry, measure that system's maximum time-to-revoke and work to shrink it (this guide's recommendation, as in [authentication.md](authentication.md)). The cited files do not establish a provider-membership removal mechanism, a session-revocation mechanism for every application, or this deployment's measured maximum time-to-revoke. For delayed invalidation checks, follow the REASONED offboarding replay check in [authentication.md](authentication.md#quick-checks): matched requests must succeed before revocation, then each old credential must be refused and return no protected data within its configured maximum time-to-revoke; these are test expectations, not observed results. [mfa.md](mfa.md) covers revoking the second factor alongside the account. + +## Common mistakes + +- Verifying only the custom domain and forgetting the platform's own generated URL, which is often still reachable and unauthenticated. +- Deleting the app first and the DNS record later, leaving the dangling-CNAME window OWASP warns about when the target can be claimed; waiting only for TTL expiry also misses the serve-stale exception in RFC 8767. +- Trusting a local `certbot renew --dry-run` as proof that production certificates are actually renewing; Certbot v2.3.0 documents test certificates without saving them, staging unless a custom server is set, and deploy hooks only with `--run-deploy-hooks` after a successful dry run, using the active certificate. This guide infers that this test does not establish whether the last real renewal succeeded. + +## Verify + +REASONED: outside-network scans, DNS retirement and certificate inspection; no deployment, authoritative DNS zone or second-network probe host was supplied for this metadata review, and no run is recorded. The commands and expected readings below follow the cited Nmap, dig and OpenSSL 3.0 documentation and the guide's lifecycle reasoning; Sources has no curl reference. DNS response interpretations follow RFC 2308 section 2, and cache timing follows the cited DNS RFCs, including RFC 8767's serve-stale exception; the authoritative-removal checks are this guide's advice and do not establish that all recursive caches have refreshed. + +```bash +# From a second network, not the host itself: +( # a subshell, so your own script arguments are untouched + set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IP' # replace inside the quotes, keeping them + [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } + shift + [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } + case "$1" in + *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; + *) nmap -Pn -p- "$1" ;; # TCP only: only the intended ports answer here; probe any UDP service separately (nmap -sU) + # and the scan actually ran: nmap reporting a host down, a permission error, or no output at all is + # inconclusive, not a clean result + esac +) +( # a subshell, so your own script arguments are untouched + set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IPV6' # replace inside the quotes, keeping them + [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } + shift + [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } + case "$1" in + *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; + *) nmap -Pn -6 -p- "$1" ;; # same, over the public IPv6 address (TCP only) + # same reading: a scan that did not run is not a clean scan + esac +) +# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv +curl -q -sI https://retired-preview.example.com/ # a connection/DNS/TLS failure here is NOT proof the record is gone (a dangling record to a released target fails the same way); use the dig check below, and confirm at the authoritative nameservers, to establish removal +# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv +for t in A AAAA CNAME; do echo "$t:"; dig +short retired-preview.example.com "$t"; done # checks your configured resolver only, so it is not authoritative: any A/AAAA/CNAME answer means that resolver still returns a mapping (investigate before release; an answer alone does not prove the target is dangling), and empty output is inconclusive. Confirm removal against each authoritative nameserver directly (dig @AUTH_NS name TYPE +norecurse), requiring NXDOMAIN or NOERROR/NODATA with no CNAME; treat SERVFAIL/referral/timeout as inconclusive +# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv +openssl s_client -connect app.example.com:443 -servername app.example.com \ + -verify_hostname app.example.com -verify_return_error +- Certbot dry-run test-server dispatch, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L210-L211 +- Certbot staging-server selection and custom-server exception, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L237-L264 +- Certbot user-guide dry-run and subsequent live-renewal example, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L786-L798 + + + +## 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/deployment-lifecycle.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): + +- nmap: Nmap documentation (unknown) +- shodan: Shodan (unknown) +- censys: Censys (unknown) +- vercel: Vercel Deployment Protection (unknown) +- access: Cloudflare Access policies (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- dns: BIND dig documentation (9.18.39) +- openssl: OpenSSL command documentation (3.0) +- openbsd-nc: OpenBSD nc manual (7.6) +- owasp-takeover: OWASP subdomain takeover prevention (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) +- wstg: OWASP Web Security Testing Guide (v4.2) +- dns-rfcs: DNS caching RFCs (unknown) +- owasp-docs: OWASP security guidance (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) +- entra-docs: Microsoft Entra revocation model (231747abc59ae3d50c74e215c1cdd592eb980723) +- cloudflare-access: Cloudflare Access and origin configuration (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- oauth2-proxy: OAuth2 Proxy (v7.15.4) +- authelia: Authelia (v4.39.28) +- oauth-revocation: OAuth 2.0 Token Revocation RFC (unknown) +- nist: NIST digital identity guidelines (SP 800-63B-4) +- ubuntu-nc: Ubuntu noble netcat-openbsd source (c047f1a4572e796c8643fb7e5a719a9d960a677b) +- c9s-nmap: CentOS Stream 9 nmap packaging (17c2b7795d821cd7fbe311cf3f885be3ea1f122f) +- busybox: BusyBox 1.36.1 (1_36_1) +- docker-notes: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) +- webui: Open WebUI (0a7c15832fb30b1903753e83f81dc7d27e5b0944) +- certbot-v230: Certbot user guide and source (v2.3.0) +- le: Let's Encrypt documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- outside (REASONED; nmap): Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour. +- tcp-scan (REASONED; nmap, openbsd-nc, ubuntu-nc, c9s-nmap, busybox): Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive. +- udp-scan (REASONED; nmap): The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU. +- all-addresses (REASONED; nmap, vercel): Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others. +- passive-index (REASONED; shodan, censys): Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan. +- initialize-secrets (REASONED; access, webui): As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here. +- registration (REASONED; access, webui): As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here. +- fronting-order (REASONED; access, le): As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice. +- setup-route (REASONED; access, webui): After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only. +- preview-standard (REASONED; vercel): Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open. +- preview-all (REASONED; vercel): All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on. +- preview-gate (REASONED; access, vercel): Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs. +- clone-secrets (REASONED; access): As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. +- dns-retirement (REASONED; dns, owasp-takeover, wstg, dns-rfcs): Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection. +- retire-credentials (REASONED; access, owasp-docs): After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users. +- reverify (REASONED; nmap, docker-notes, webui): Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only. +- certificate-monitor (REASONED; nmap, openssl, certbot-v230): Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only. +- offboarding (REASONED; access, owasp-docs, entra-docs, cloudflare-access, oauth2-proxy, authelia, oauth-revocation, nist): When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations. +- verify-ipv4 (REASONED; nmap): From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive. +- verify-ipv6 (REASONED; nmap): Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive. +- verify-dns (REASONED; dns, dns-rfcs): curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive. +- verify-certificate (REASONED; nmap, openssl): The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only. + +````json +{ "schema": 1, "checked": "2026-10-18", "documentation_checked": "2026-09", @@ -231,209 +461,5 @@ version_basis: { "verify-certificate": {"text": "The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only.", "components": ["nmap", "openssl"], "sources": ["nmap:sf8404f695ccd", "openssl:s00aaf106164f", "openssl:s0856f4a58574"], "status": "REASONED", "verify": [1]} } } ---- -# Deploying safely over time: verify from outside, safe order, previews, teardown - - -**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): - -- nmap: Nmap documentation (unknown) -- shodan: Shodan (unknown) -- censys: Censys (unknown) -- vercel: Vercel Deployment Protection (unknown) -- access: Cloudflare Access policies (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- dns: BIND dig documentation (9.18.39) -- openssl: OpenSSL command documentation (3.0) -- openbsd-nc: OpenBSD nc manual (7.6) -- owasp-takeover: OWASP subdomain takeover prevention (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) -- wstg: OWASP Web Security Testing Guide (v4.2) -- dns-rfcs: DNS caching RFCs (unknown) -- owasp-docs: OWASP security guidance (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) -- entra-docs: Microsoft Entra revocation model (231747abc59ae3d50c74e215c1cdd592eb980723) -- cloudflare-access: Cloudflare Access and origin configuration (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- oauth2-proxy: OAuth2 Proxy (v7.15.4) -- authelia: Authelia (v4.39.28) -- oauth-revocation: OAuth 2.0 Token Revocation RFC (unknown) -- nist: NIST digital identity guidelines (SP 800-63B-4) -- ubuntu-nc: Ubuntu noble netcat-openbsd source (c047f1a4572e796c8643fb7e5a719a9d960a677b) -- c9s-nmap: CentOS Stream 9 nmap packaging (17c2b7795d821cd7fbe311cf3f885be3ea1f122f) -- busybox: BusyBox 1.36.1 (1_36_1) -- docker-notes: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) -- webui: Open WebUI (0a7c15832fb30b1903753e83f81dc7d27e5b0944) -- certbot-v230: Certbot user guide and source (v2.3.0) -- le: Let's Encrypt documentation (unknown) - -Claims (full source mapping in the front matter): - -- outside (REASONED; nmap): Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour. -- tcp-scan (REASONED; nmap, openbsd-nc, ubuntu-nc, c9s-nmap, busybox): Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive. -- udp-scan (REASONED; nmap): The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU. -- all-addresses (REASONED; nmap, vercel): Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others. -- passive-index (REASONED; shodan, censys): Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan. -- initialize-secrets (REASONED; access, webui): As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here. -- registration (REASONED; access, webui): As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here. -- fronting-order (REASONED; access, le): As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice. -- setup-route (REASONED; access, webui): After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only. -- preview-standard (REASONED; vercel): Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open. -- preview-all (REASONED; vercel): All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on. -- preview-gate (REASONED; access, vercel): Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs. -- clone-secrets (REASONED; access): As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. -- dns-retirement (REASONED; dns, owasp-takeover, wstg, dns-rfcs): Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection. -- retire-credentials (REASONED; access, owasp-docs): After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users. -- reverify (REASONED; nmap, docker-notes, webui): Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only. -- certificate-monitor (REASONED; nmap, openssl, certbot-v230): Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only. -- offboarding (REASONED; access, owasp-docs, entra-docs, cloudflare-access, oauth2-proxy, authelia, oauth-revocation, nist): When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations. -- verify-ipv4 (REASONED; nmap): From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive. -- verify-ipv6 (REASONED; nmap): Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive. -- verify-dns (REASONED; dns, dns-rfcs): curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive. -- verify-certificate (REASONED; nmap, openssl): The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only. +```` - -This repository's per-service guides check TLS, binding, and authentication from inside the host and at the moment you set them up. Real exposure is judged from outside the host, and it drifts: a preview URL, a restored backup, or an upgrade can reopen something already closed. This guide is the lifecycle layer around the per-service checks. - -## 1. Verify from outside, not just from the host - -A `curl` or `ss -tlnp` run on the host itself cannot see a firewall the host does not know about, or a platform-level bypass like Docker's iptables rules ([docker.md](docker.md)). Verify from a second network: a phone hotspot, a cloud shell, or a second VM. - -- Port-by-port: `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 REPLACE_WITH_YOUR_PUBLIC_IP "$p"; done`, or a full sweep, `nmap -Pn -p- REPLACE_WITH_YOUR_PUBLIC_IP` (per the nmap reference, which documents `-p-` and `-p 1-65535` as equivalent port-range syntax; this sweeps TCP only, so inventory and separately probe any UDP service with `nmap -sU` on its ports; only scan hosts you own or are authorized to test). Before running either form, use the [guarded Verify block](#verify); for the loop, replace only its IPv4 arm's `nmap -Pn -p- "$1"` with `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 "$1" "$p"; done`, keeping the whole guard and substituting inside the quotes on `set --`. Ubuntu noble netcat-openbsd documents `-v` as verbose output, `-z` as scanning without sending data and `-w` as a connection/idle timeout. BusyBox 1.36.1 small `nc` lacks `-v` and `-z`, and has `-w` only with `NC_EXTRA`; its `NC_110_COMPAT` applet lists `-v` and `-w`, with `-z` conditional on `NC_EXTRA`. CentOS Stream 9 packages Ncat 7.92 as an `nc` alternative; Ncat 7.92's option behaviour is not established here, nor is traditional or GNU netcat behaviour. Check the installed variant. As this guide's assessment, refusal or timeout shows only that this connection failed; an unsupported-option error, local error or silent exit is inconclusive. -- Check every address the service actually has, not just the one you remember configuring: the public IPv4 address, the public IPv6 address if the host has one, and any platform-assigned URL alongside your custom domain (a PaaS default subdomain, per [paas.md](paas.md), often stays reachable even when the custom domain is fronted). A scan of one address that misses the others is not a clean result, it is an incomplete one. -- Cross-reference what is already indexed about your IP with a passive internet-wide scanner such as Shodan or Censys; both build a continuously updated index of internet-connected hosts and services, so a stale exposure can show up there before you find it yourself ([cloud-firewalls.md](cloud-firewalls.md) covers the firewall rules this is checking). - -## 2. Safe initialization order - -This guide advises the following first-deployment order, keeping application access closed until initialization and the fronting gate are ready: - -1. Create the owner or admin account's credentials and any signing secrets (session, JWT, or cookie-signing keys) per [authentication.md](authentication.md); the ordering is this guide's advice, and signing secrets follow that guide's recommendation. -2. Set the registration policy (disable open self-registration, or restrict it to an allowlisted domain) before opening application access, as this guide advises; [authentication.md](authentication.md) covers allowlisting after federated login. -3. Put the fronting gate in place, TLS and any proxy-level authentication, per [free-certificates.md](free-certificates.md) and the fronting-layer guides. For ACME, this guide advises DNS-01 issuance before opening inbound access, or opening only the challenge port during issuance while keeping application access blocked: HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443, whereas DNS-01 needs no inbound port, per the [prerequisite list](free-certificates.md#prerequisites). These challenge requirements do not establish this deployment order. Cloudflare recommends creating the Access application before the tunnel route and warns that without it the published application is available to anyone on the Internet; an already publicly routable origin needs its IP protected by other methods, per the pinned self-hosted application instructions. -4. Only then open application network access, as this guide advises. - -For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin, open to whoever completes it first. [Open WebUI](open-webui.md) at commit 0a7c15832fb30b1903753e83f81dc7d27e5b0944 is one example: its signup handler makes the first account admin; with authentication enabled, ENABLE_SIGNUP and ENABLE_LOGIN_FORM default to true, and an empty database bypasses ENABLE_SIGNUP when the login form is enabled. Once accounts exist, disabled signup refuses registration. These Open WebUI paths do not establish other applications' bootstrap, signing-secret, registration or domain-enforcement behaviour. After initialization, confirm the setup route itself is closed: restart the service and try hitting the initial-setup URL again; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). The Open WebUI signup handler illustrates bootstrap and signup refusal, not every application's setup route or a deployed restart result. - -## 3. Previews and clones get production posture - -A preview deployment or a database clone is not lower stakes just because it is temporary. Vercel's Deployment Protection illustrates the gap: Standard Protection, available on every plan, gates preview and generated deployment URLs but leaves the production domain open by default; the All Deployments scope closes that gap too, and Vercel's September 9, 2026 change made pairing it with Vercel Authentication free on every plan rather than Pro and Enterprise only ([paas.md](paas.md); per Vercel's Deployment Protection changelog of 9 September 2026 and its current Deployment Protection reference, which states that Vercel Authentication for All Deployments does not require a paid add-on). Where the platform's own gate does not cover a hostname, front it the same way as production, for example a Cloudflare Access policy scoped to that preview hostname, per the pinned self-hosted application instructions. Those instructions establish the Access gate, not the platform's coverage of preview URLs. Either way, as this guide's advice, treat a clone's data the same as the original: rotate any credential a clone inherited if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. - -## 4. Teardown: DNS before the app, then revoke the rest - -Retiring a deployment in the wrong order can leave a dangling DNS record pointing at a resource someone else can claim (a subdomain takeover); OWASP's Subdomain Takeover Prevention Cheat Sheet describes this mechanism, and WSTG v4.2 identifies an inactive target and inadequate ownership verification as preconditions. Delete the DNS record (the `CNAME` or `A`/`AAAA`) before you delete or release the underlying app, load balancer, or IP, and wait at least the record's old TTL before decommissioning the resource, following OWASP's order; WSTG supports removing vulnerable records but does not specify that wait. - -RFC 1034 section 3.6 and RFC 1035 section 3.2.1 describe resolver caching and TTL, with TTL bounds clarified by RFC 2181 section 8 and updated by RFC 8767 section 4. **TTL expiry is not a hard bound: RFC 8767 permits serving stale records when they cannot be authoritatively refreshed.** This guide infers that deleting a record does not flush cached answers, so immediately releasing a reclaimable IP or hostname can still send cached clients to its next owner. OWASP describes claimable CNAME targets and released IPs that an attacker obtains after reassignment; the cited files do not establish a particular platform's current reclaimability. This guide advises keeping the resource reserved through the old TTL and until removal is confirmed at every authoritative nameserver, and accounting for serve-stale before release; authoritative removal and elapsed TTL do not prove that every resolver has stopped returning the old mapping. - -Then revoke what pointed at it: access policies (Cloudflare Access or equivalent), API tokens and service credentials scoped to that deployment ([machine-auth.md](machine-auth.md)), and database users created only for it. This remains the guide's retirement requirement: OWASP Secrets Management requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only, and the cited files do not establish removal of an Access application or policy or dropping database users. - -## 5. Re-verify after anything changes - -Rerun the outside-in checks in section 1 and the negative auth tests from [authentication.md](authentication.md) after an upgrade, a backup restore (which, as this guide's assessment, can reintroduce a default account or reset a feature flag), or any change to network policy or auth configuration. Docker's pinned 28.x release notes give concrete change-related cases: 28.0.0 sets IPv6 `FORWARD` policy to `DROP` only when Docker enables forwarding itself, warning that hosts with forwarding already enabled that relied on Docker setting `DROP` may need configuration changes; 28.3.3 fixes direct local-network access to published container ports after a firewalld reload, even when intended for loopback-only access. Open WebUI's pinned configuration supplies an environment-derived signup default and enables persistence by default. As this guide's conditional inference for restores, if database values override the environment after first launch as described in [open-webui.md](open-webui.md), restoring earlier database values could reinstate earlier signup settings; the configuration this guide cites delegates to `models.config.Config`, whose implementation this guide does not cite, so these files do not establish that precedence or general restore behaviour. Nmap supports scanning, not application restore behaviour. Monitor certificate expiry from outside the host too: as this guide's assessment, a renewal cron can silently fail while the host's own view still looks fine, so this guide's advice is an external check (a scheduled `openssl s_client` from another machine, or a third-party uptime/certificate monitor). Certbot v2.3.0's `certbot renew --dry-run` uses the staging test server unless a custom server is set, obtains test certificates without saving them and does not run deploy hooks unless `--run-deploy-hooks` is supplied; applicable deploy hooks then run only after a successful dry run, using the current active certificate. The option documentation also warns that dry runs can temporarily change webserver configuration, reload webservers and run pre/post hooks; the user guide's dry-run example distinguishes it from a subsequent live renewal. From these documented limits, this guide infers that a dry run cannot show whether the last real renewal succeeded; Nmap is outside-check context only. - -## 6. Offboarding - -When a person leaves, revoke access at every layer they touched, not only their password: identity-provider membership (the SSO or directory account), proxy-level sessions (Cloudflare Access or equivalent), application-level sessions on each service, and any personal access tokens or API keys issued to them (this guide's recommendation, as in [authentication.md](authentication.md)). - -Microsoft Entra documents blocking new sign-ins and revoking refresh tokens, but the application must revoke its own session; access-token lifetimes vary, and CAE critical-event enforcement can take up to 15 minutes for supported services. Cloudflare Access documents per-user session revocation, but an active IdP profile can start a new session; after user logout, previously issued tokens stop being accepted in 20-30 seconds. OAuth2 Proxy v7.15.4 documents cookie expiry and refresh, and Authelia v4.39.28 documents cookie expiry and session destruction, overridden by remember-me; these passages do not establish immediate per-user revocation for either proxy. RFC 7009 section 2.1 specifies immediate token invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST SP 800-63B-4 section 4.5 requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. - -Where a system cannot revoke immediately, such as a session cached until its own expiry, measure that system's maximum time-to-revoke and work to shrink it (this guide's recommendation, as in [authentication.md](authentication.md)). The cited files do not establish a provider-membership removal mechanism, a session-revocation mechanism for every application, or this deployment's measured maximum time-to-revoke. For delayed invalidation checks, follow the REASONED offboarding replay check in [authentication.md](authentication.md#quick-checks): matched requests must succeed before revocation, then each old credential must be refused and return no protected data within its configured maximum time-to-revoke; these are test expectations, not observed results. [mfa.md](mfa.md) covers revoking the second factor alongside the account. - -## Common mistakes - -- Verifying only the custom domain and forgetting the platform's own generated URL, which is often still reachable and unauthenticated. -- Deleting the app first and the DNS record later, leaving the dangling-CNAME window OWASP warns about when the target can be claimed; waiting only for TTL expiry also misses the serve-stale exception in RFC 8767. -- Trusting a local `certbot renew --dry-run` as proof that production certificates are actually renewing; Certbot v2.3.0 documents test certificates without saving them, staging unless a custom server is set, and deploy hooks only with `--run-deploy-hooks` after a successful dry run, using the active certificate. This guide infers that this test does not establish whether the last real renewal succeeded. - -## Verify - -REASONED: outside-network scans, DNS retirement and certificate inspection; no deployment, authoritative DNS zone or second-network probe host was supplied for this metadata review, and no run is recorded. The commands and expected readings below follow the cited Nmap, dig and OpenSSL 3.0 documentation and the guide's lifecycle reasoning; Sources has no curl reference. DNS response interpretations follow RFC 2308 section 2, and cache timing follows the cited DNS RFCs, including RFC 8767's serve-stale exception; the authoritative-removal checks are this guide's advice and do not establish that all recursive caches have refreshed. - -```bash -# From a second network, not the host itself: -( # a subshell, so your own script arguments are untouched - set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IP' # replace inside the quotes, keeping them - [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } - shift - [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } - case "$1" in - *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; - *) nmap -Pn -p- "$1" ;; # TCP only: only the intended ports answer here; probe any UDP service separately (nmap -sU) - # and the scan actually ran: nmap reporting a host down, a permission error, or no output at all is - # inconclusive, not a clean result - esac -) -( # a subshell, so your own script arguments are untouched - set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IPV6' # replace inside the quotes, keeping them - [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } - shift - [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } - case "$1" in - *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; - *) nmap -Pn -6 -p- "$1" ;; # same, over the public IPv6 address (TCP only) - # same reading: a scan that did not run is not a clean scan - esac -) -# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv -curl -q -sI https://retired-preview.example.com/ # a connection/DNS/TLS failure here is NOT proof the record is gone (a dangling record to a released target fails the same way); use the dig check below, and confirm at the authoritative nameservers, to establish removal -# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv -for t in A AAAA CNAME; do echo "$t:"; dig +short retired-preview.example.com "$t"; done # checks your configured resolver only, so it is not authoritative: any A/AAAA/CNAME answer means that resolver still returns a mapping (investigate before release; an answer alone does not prove the target is dangling), and empty output is inconclusive. Confirm removal against each authoritative nameserver directly (dig @AUTH_NS name TYPE +norecurse), requiring NXDOMAIN or NOERROR/NODATA with no CNAME; treat SERVFAIL/referral/timeout as inconclusive -# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv -openssl s_client -connect app.example.com:443 -servername app.example.com \ - -verify_hostname app.example.com -verify_return_error -- Certbot dry-run test-server dispatch, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L210-L211 -- Certbot staging-server selection and custom-server exception, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L237-L264 -- Certbot user-guide dry-run and subsequent live-renewal example, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L786-L798 diff --git a/devops-uis.md b/devops-uis.md index ea2312918..dd060d81d 100644 --- a/devops-uis.md +++ b/devops-uis.md @@ -1,343 +1,12 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "7cea8ce7dc2689ed90673f4d926b8259c0c8f0b24281b47ea184dfe91ffa1289", - "components": { - "portainer": { - "name": "Portainer", - "basis": "unknown", - "sources": { - "s3e163c7d3c38": "https://docs.portainer.io/start/install-ce/server/docker/linux", - "s115489d2f86f": "https://docs.portainer.io/start/install-ce/server/setup", - "s59297757a042": "https://docs.portainer.io/admin/settings/authentication", - "s8901bbe7c3d3": "https://docs.portainer.io/admin/settings/authentication/oauth" - } - }, - "coolify": { - "name": "Coolify", - "basis": "unknown", - "sources": { - "s41e2d21202d9": "https://coolify.io/docs/start-with-self-hosted", - "sf2ae62425b35": "https://coolify.io/docs/core/infrastructure/servers/firewall", - "sf5d819d5d87d": "https://coolify.io/docs/core/networking/proxy/overview", - "s90addf9d8d5d": "https://coolify.io/docs/core/networking/dns", - "s683244a61c48": "https://coolify.io/docs/core/infrastructure/servers/openssh" - } - }, - "dokploy": { - "name": "Dokploy", - "basis": "unknown", - "sources": { - "sc2cc854c9b7e": "https://docs.dokploy.com/docs/core/installation" - } - }, - "npm": { - "name": "Nginx Proxy Manager", - "basis": "unknown", - "sources": { - "se553d384e783": "https://nginxproxymanager.com/setup/" - } - }, - "vaultwarden": { - "name": "Vaultwarden docs", - "basis": "unknown", - "sources": { - "s0bbe8ad856cd": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page", - "s6d3d0d02dcc9": "https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users", - "s60d064cae50e": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS" - } - }, - "vaultwarden-pin": { - "name": "Vaultwarden config", - "basis": "3347698712d3e99e652ad4394ef2fc0ce2800fdd", - "sources": { - "s31c5fc79f5a0": "https://github.com/dani-garcia/vaultwarden/blob/3347698712d3e99e652ad4394ef2fc0ce2800fdd/.env.template" - } - }, - "dashboard": { - "name": "Dashboard docs", - "basis": "unknown", - "sources": { - "s73a36a43f6b5": "https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/" - } - }, - "dashboard-old": { - "name": "Dashboard arguments", - "basis": "v2.7.0", - "sources": { - "sacea2944651e": "https://github.com/kubernetes-retired/dashboard/blob/v2.7.0/docs/common/dashboard-arguments.md" - } - }, - "jenkins": { - "name": "Jenkins", - "basis": "unknown", - "sources": { - "s524c8d5871f1": "https://www.jenkins.io/doc/book/security/managing-security/", - "sdfee751aeeee": "https://www.jenkins.io/doc/book/security/access-control/", - "s6f5287fd36d4": "https://www.jenkins.io/doc/book/security/csrf-protection/" - } - }, - "gitea": { - "name": "Gitea docs", - "basis": "unknown", - "sources": { - "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet", - "s3b7060d58383": "https://docs.gitea.com/usage/user-setting/multi-factor-authentication/" - } - }, - "gitlab": { - "name": "GitLab CE", - "basis": "unknown", - "sources": { - "s3b7ac561a92e": "https://docs.gitlab.com/install/docker/installation/", - "s6e8d16cad2ef": "https://docs.gitlab.com/administration/settings/sign_up_restrictions/", - "s6b8a68a77e17": "https://docs.gitlab.com/administration/settings/visibility_and_access_controls/", - "s0347014ce554": "https://docs.gitlab.com/security/two_factor_authentication/" - } - }, - "kuma": { - "name": "Uptime Kuma", - "basis": "unknown", - "sources": { - "s05b853ba300b": "https://github.com/louislam/uptime-kuma", - "sed52bfca4039": "https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" - } - }, - "docker": { - "name": "Docker Engine", - "basis": "unknown", - "sources": { - "sfe15a8156c18": "https://docs.docker.com/engine/security/protect-access/", - "s1b5ec54421b0": "https://docs.docker.com/engine/daemon/remote-access/" - } - }, - "dozzle": { - "name": "Dozzle", - "basis": "unknown", - "sources": { - "s45b3eab2515b": "https://dozzle.dev/guide/authentication" - } - }, - "filebrowser": { - "name": "Filebrowser", - "basis": "unknown", - "sources": { - "s6386c9e2356b": "https://github.com/filebrowser/filebrowser" - } - }, - "nodered": { - "name": "Node-RED", - "basis": "unknown", - "sources": { - "sa94f329023fe": "https://nodered.org/docs/user-guide/runtime/securing-node-red" - } - }, - "gitea-pin": { - "name": "Gitea listener", - "basis": "v1.27.3", - "sources": { - "s31dd622b1c12": "https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122" - } - }, - "gitea-mfa-min": { - "name": "Gitea MFA minimum", - "basis": "1.24", - "sources": { - "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet" - } - }, - "registry-docs": { - "name": "Docker Registry documentation (rolling)", - "basis": "unknown", - "sources": { - "sa315b3f8bfa6": "https://distribution.github.io/distribution/about/deploying/" - } - }, - "htpasswd": { - "name": "Apache htpasswd", - "basis": "2.4", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "owasp-passwords": { - "name": "OWASP password storage", - "basis": "1a3f58ed2714f258a1d713966457f1a0f426e841", - "sources": { - "s0f70ec4aa271": "https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139" - } - } - }, - "claims": { - "private": {"text": "Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, - "portainer-ports": {"text": "UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute.", "components": ["portainer"], "sources": ["portainer:s3e163c7d3c38"], "status": "REASONED"}, - "portainer-setup": {"text": "Setup requires the logged setup_token; first user is admin with a password of at least 12 characters.", "components": ["portainer"], "sources": ["portainer:s115489d2f86f"], "status": "REASONED"}, - "portainer-auth": {"text": "Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider.", "components": ["portainer"], "sources": ["portainer:s59297757a042", "portainer:s8901bbe7c3d3"], "status": "REASONED"}, - "portainer-socket": {"text": "Mounted docker.sock grants host-root power; treat a Portainer admin accordingly.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, - "coolify-setup": {"text": "HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately.", "components": ["coolify"], "sources": ["coolify:s41e2d21202d9"], "status": "REASONED"}, - "coolify-tls": {"text": "Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35", "coolify:sf5d819d5d87d", "coolify:s90addf9d8d5d"], "status": "REASONED"}, - "coolify-firewall": {"text": "Docker bypasses UFW; restrict dashboard ports at the cloud firewall.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35"], "status": "REASONED"}, - "coolify-ssh": {"text": "Server SSH keys require no passphrase; stored key material is the whole secret.", "components": ["coolify"], "sources": ["coolify:s683244a61c48"], "status": "REASONED"}, - "dokploy-ports": {"text": "UI is 3000, Traefik 80/443; first setup creates admin.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, - "dokploy-tls": {"text": "Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, - "npm-ports": {"text": "Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, - "npm-admin": {"text": "Change default first-run admin credentials before other work.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, - "vaultwarden-tls": {"text": "Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s60d064cae50e", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-signup": {"text": "SIGNUPS_ALLOWED defaults true; set false to close registration.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-invites": {"text": "INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-admin": {"text": "ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-session": {"text": "Admin sessions expire after 20 minutes by default.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-port": {"text": "Container port 80 or non-Docker 8000; publish only to loopback/proxy network.", "components": ["vaultwarden-pin"], "sources": ["vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "dashboard-lifecycle": {"text": "Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "dashboard-forward": {"text": "Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "dashboard-token": {"text": "Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "dashboard-old-flags": {"text": "Older 2.x enable-skip-login and enable-insecure-login default false; keep both false.", "components": ["dashboard-old"], "sources": ["dashboard-old:sacea2944651e"], "status": "REASONED"}, - "dashboard-mfa": {"text": "API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "jenkins-auth": {"text": "Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, - "jenkins-matrix": {"text": "Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control.", "components": ["jenkins"], "sources": ["jenkins:sdfee751aeeee"], "status": "REASONED"}, - "jenkins-csrf": {"text": "Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch.", "components": ["jenkins"], "sources": ["jenkins:s6f5287fd36d4"], "status": "REASONED"}, - "jenkins-agents": {"text": "Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1"], "status": "REASONED"}, - "jenkins-mfa": {"text": "No native second factor; enforce MFA through SSO/fronting identity.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, - "gitea-bind": {"text": "HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy.", "components": ["gitea-pin"], "sources": ["gitea-pin:s31dd622b1c12"], "status": "REASONED"}, - "gitea-tls": {"text": "Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, - "gitea-registration": {"text": "Set service DISABLE_REGISTRATION=true; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, - "gitea-private": {"text": "Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, - "gitea-mfa": {"text": "Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later.", "components": ["gitea", "gitea-mfa-min"], "sources": ["gitea:s57933519c308", "gitea:s3b7060d58383", "gitea-mfa-min:s57933519c308"], "status": "REASONED"}, - "gitea-tokens": {"text": "With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens.", "components": ["gitea"], "sources": ["gitea:s3b7060d58383"], "status": "REASONED"}, - "gitlab-root": {"text": "Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED"}, - "gitlab-signup": {"text": "signup_enabled defaults true; disable for a private forge.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, - "gitlab-approval": {"text": "New instances require admin approval after signup by default; do not rely on approval alone.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, - "gitlab-visibility": {"text": "Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants.", "components": ["gitlab"], "sources": ["gitlab:s6b8a68a77e17"], "status": "REASONED"}, - "gitlab-mfa": {"text": "Enforce two-factor authentication for every user; available on all tiers.", "components": ["gitlab"], "sources": ["gitlab:s0347014ce554"], "status": "REASONED"}, - "kuma-proxy": {"text": "Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b", "kuma:sed52bfca4039"], "status": "REASONED"}, - "kuma-admin": {"text": "Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b"], "status": "REASONED"}, - "docker-root": {"text": "Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375.", "components": ["docker"], "sources": ["docker:sfe15a8156c18", "docker:s1b5ec54421b0"], "status": "REASONED"}, - "docker-ssh": {"text": "DOCKER_HOST/context can select SSH to the Unix socket without a new listener.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, - "docker-mtls": {"text": "2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, - "dozzle-socket": {"text": "Docker-socket access has host-level power; keep Dozzle private with fronting MFA.", "components": ["dozzle", "docker"], "sources": ["dozzle:s45b3eab2515b", "docker:sfe15a8156c18"], "status": "REASONED"}, - "dozzle-auth": {"text": "Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, - "dozzle-actions": {"text": "Leave optional container actions/shell off unless needed; either enables remote command execution.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, - "registry-auth": {"text": "registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, - "registry-bcrypt": {"text": "Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ.", "components": ["registry-docs", "htpasswd", "owasp-passwords"], "sources": ["registry-docs:sa315b3f8bfa6", "htpasswd:scde9004bb967", "owasp-passwords:s0f70ec4aa271"], "status": "REASONED"}, - "registry-private": {"text": "Use private networking and tunnels/Access with fronting MFA.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, - "filebrowser-admin": {"text": "Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, - "filebrowser-private": {"text": "Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, - "nodered-default": {"text": "Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "nodered-auth": {"text": "Set adminAuth with bcrypt passwords from node-red admin hash-pw.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "nodered-secret": {"text": "Set credentialSecret yourself; otherwise generated, and stored credentials depend on it.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "nodered-private": {"text": "Keep editor private with fronting MFA; it has the power of its flows.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "verify-listeners": {"text": "Read every panel/API listener; expect loopback or absence, never wildcard exposure.", "components": ["portainer", "dokploy", "npm", "kuma", "docker"], "sources": ["portainer:s3e163c7d3c38", "dokploy:sc2cc854c9b7e", "npm:se553d384e783", "kuma:s05b853ba300b", "docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, - "verify-panel": {"text": "Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded.", "components": ["portainer", "jenkins", "gitea"], "sources": ["portainer:s3e163c7d3c38", "jenkins:sdfee751aeeee", "gitea:s57933519c308"], "status": "REASONED", "verify": [1]}, - "verify-docker-plain": {"text": "Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive.", "components": ["docker"], "sources": ["docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, - "verify-docker-mtls": {"text": "2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED", "verify": [1]}, - "verify-gitlab": {"text": "Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED", "verify": [1]} - } -} ---- # DevOps panels: Portainer, Coolify, Dokploy, Nginx Proxy Manager, Vaultwarden, Kubernetes Dashboard, Jenkins, Gitea, Uptime Kuma, and the Docker API - -**Version basis** +Written against Portainer (no recorded version), Dashboard arguments v2.7.0, Gitea listener v1.27.3, Gitea MFA minimum 1.24, and Apache htpasswd 2.4; 2 further sources are pinned to a commit and 14 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 62 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-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. +These panels control hosts, containers, clusters, deploy keys, and secrets; a login to one of them is a login to everything it manages. One rule dominates everything tool-specific below: **a DevOps panel is never reachable from the public internet.** Bind it to loopback or a private interface and reach it through SSH port forwarding, a tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything that must be public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) and the admin login gets MFA ([mfa.md](mfa.md)). Several of these tools create their administrator on first visit, so whoever reaches a fresh install first owns it: create the account before the port is reachable by anyone else. -Components (recorded basis): - -- portainer: Portainer (unknown) -- coolify: Coolify (unknown) -- dokploy: Dokploy (unknown) -- npm: Nginx Proxy Manager (unknown) -- vaultwarden: Vaultwarden docs (unknown) -- vaultwarden-pin: Vaultwarden config (3347698712d3e99e652ad4394ef2fc0ce2800fdd) -- dashboard: Dashboard docs (unknown) -- dashboard-old: Dashboard arguments (v2.7.0) -- jenkins: Jenkins (unknown) -- gitea: Gitea docs (unknown) -- gitlab: GitLab CE (unknown) -- kuma: Uptime Kuma (unknown) -- docker: Docker Engine (unknown) -- dozzle: Dozzle (unknown) -- filebrowser: Filebrowser (unknown) -- nodered: Node-RED (unknown) -- gitea-pin: Gitea listener (v1.27.3) -- gitea-mfa-min: Gitea MFA minimum (1.24) -- registry-docs: Docker Registry documentation (rolling) (unknown) -- htpasswd: Apache htpasswd (2.4) -- owasp-passwords: OWASP password storage (1a3f58ed2714f258a1d713966457f1a0f426e841) - -Claims (full source mapping in the front matter): - -- private (REASONED; portainer, docker): Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure. -- portainer-ports (REASONED; portainer): UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute. -- portainer-setup (REASONED; portainer): Setup requires the logged setup_token; first user is admin with a password of at least 12 characters. -- portainer-auth (REASONED; portainer): Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider. -- portainer-socket (REASONED; portainer, docker): Mounted docker.sock grants host-root power; treat a Portainer admin accordingly. -- coolify-setup (REASONED; coolify): HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately. -- coolify-tls (REASONED; coolify): Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close. -- coolify-firewall (REASONED; coolify): Docker bypasses UFW; restrict dashboard ports at the cloud firewall. -- coolify-ssh (REASONED; coolify): Server SSH keys require no passphrase; stored key material is the whole secret. -- dokploy-ports (REASONED; dokploy): UI is 3000, Traefik 80/443; first setup creates admin. -- dokploy-tls (REASONED; dokploy): Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000. -- npm-ports (REASONED; npm): Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in. -- npm-admin (REASONED; npm): Change default first-run admin credentials before other work. -- vaultwarden-tls (REASONED; vaultwarden, vaultwarden-pin): Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com. -- vaultwarden-signup (REASONED; vaultwarden, vaultwarden-pin): SIGNUPS_ALLOWED defaults true; set false to close registration. -- vaultwarden-invites (REASONED; vaultwarden, vaultwarden-pin): INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains. -- vaultwarden-admin (REASONED; vaultwarden, vaultwarden-pin): ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$. -- vaultwarden-session (REASONED; vaultwarden, vaultwarden-pin): Admin sessions expire after 20 minutes by default. -- vaultwarden-port (REASONED; vaultwarden-pin): Container port 80 or non-Docker 8000; publish only to loopback/proxy network. -- dashboard-lifecycle (REASONED; dashboard): Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp. -- dashboard-forward (REASONED; dashboard): Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine. -- dashboard-token (REASONED; dashboard): Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only. -- dashboard-old-flags (REASONED; dashboard-old): Older 2.x enable-skip-login and enable-insecure-login default false; keep both false. -- dashboard-mfa (REASONED; dashboard): API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI. -- jenkins-auth (REASONED; jenkins): Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too. -- jenkins-matrix (REASONED; jenkins): Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control. -- jenkins-csrf (REASONED; jenkins): Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch. -- jenkins-agents (REASONED; jenkins): Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port. -- jenkins-mfa (REASONED; jenkins): No native second factor; enforce MFA through SSO/fronting identity. -- gitea-bind (REASONED; gitea-pin): HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy. -- gitea-tls (REASONED; gitea): Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE. -- gitea-registration (REASONED; gitea): Set service DISABLE_REGISTRATION=true; default false. -- gitea-private (REASONED; gitea): Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false. -- gitea-mfa (REASONED; gitea, gitea-mfa-min): Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later. -- gitea-tokens (REASONED; gitea): With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens. -- gitlab-root (REASONED; gitlab): Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG. -- gitlab-signup (REASONED; gitlab): signup_enabled defaults true; disable for a private forge. -- gitlab-approval (REASONED; gitlab): New instances require admin approval after signup by default; do not rely on approval alone. -- gitlab-visibility (REASONED; gitlab): Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants. -- gitlab-mfa (REASONED; gitlab): Enforce two-factor authentication for every user; available on all tiers. -- kuma-proxy (REASONED; kuma): Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required. -- kuma-admin (REASONED; kuma): Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public. -- docker-root (REASONED; docker): Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375. -- docker-ssh (REASONED; docker): DOCKER_HOST/context can select SSH to the Unix socket without a new listener. -- docker-mtls (REASONED; docker): 2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too. -- dozzle-socket (REASONED; dozzle, docker): Docker-socket access has host-level power; keep Dozzle private with fronting MFA. -- dozzle-auth (REASONED; dozzle): Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy. -- dozzle-actions (REASONED; dozzle): Leave optional container actions/shell off unless needed; either enables remote command execution. -- registry-auth (REASONED; registry-docs): registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution. -- registry-bcrypt (REASONED; registry-docs, htpasswd, owasp-passwords): Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ. -- registry-private (REASONED; registry-docs): Use private networking and tunnels/Access with fronting MFA. -- filebrowser-admin (REASONED; filebrowser): Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure. -- filebrowser-private (REASONED; filebrowser): Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA. -- nodered-default (REASONED; nodered): Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification. -- nodered-auth (REASONED; nodered): Set adminAuth with bcrypt passwords from node-red admin hash-pw. -- nodered-secret (REASONED; nodered): Set credentialSecret yourself; otherwise generated, and stored credentials depend on it. -- nodered-private (REASONED; nodered): Keep editor private with fronting MFA; it has the power of its flows. -- verify-listeners (REASONED; portainer, dokploy, npm, kuma, docker): Read every panel/API listener; expect loopback or absence, never wildcard exposure. -- verify-panel (REASONED; portainer, jenkins, gitea): Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded. -- verify-docker-plain (REASONED; docker): Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive. -- verify-docker-mtls (REASONED; docker): 2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200. -- verify-gitlab (REASONED; gitlab): Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result. - - -These panels control hosts, containers, clusters, deploy keys, and secrets; a login to one of them is a login to everything it manages. One rule dominates everything tool-specific below: **a DevOps panel is never reachable from the public internet.** Bind it to loopback or a private interface and reach it through SSH port forwarding, a tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything that must be public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) and the admin login gets MFA ([mfa.md](mfa.md)). Several of these tools create their administrator on first visit, so whoever reaches a fresh install first owns it: create the account before the port is reachable by anyone else. - -## Portainer +## Portainer - Serves the UI over HTTPS on `9443` (self-signed by default; supply your own certificate or front it per [nginx.md](nginx.md)). `9000` is the legacy HTTP port and stays unpublished; `8000` is the Edge agent tunnel and is only published when Edge Compute is in use. - New instances require a setup token to complete first-time setup; it is in the server logs on the `setup_token=` line. The first user is an administrator, and its password must be at least 12 characters. @@ -509,3 +178,360 @@ REASONED: preceding block; listener and panel-login checks, Docker 2375 refusal - Gitea `HTTP_ADDR` default `0.0.0.0` and `HTTP_PORT` default `3000` (pinned tag v1.27.3): https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122 - Apache 2.4 `htpasswd` bcrypt default cost and `-C` override (checked October 2026): https://httpd.apache.org/docs/2.4/programs/htpasswd.html - OWASP password storage at commit 1a3f58ed2714f258a1d713966457f1a0f426e841 (bcrypt minimum work factor 10) (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139 + + + +## 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/devops-uis.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): + +- portainer: Portainer (unknown) +- dashboard-old: Dashboard arguments (v2.7.0) +- gitea-pin: Gitea listener (v1.27.3) +- gitea-mfa-min: Gitea MFA minimum (1.24) +- coolify: Coolify (unknown) +- dokploy: Dokploy (unknown) +- npm: Nginx Proxy Manager (unknown) +- vaultwarden: Vaultwarden docs (unknown) +- vaultwarden-pin: Vaultwarden config (3347698712d3e99e652ad4394ef2fc0ce2800fdd) +- dashboard: Dashboard docs (unknown) +- jenkins: Jenkins (unknown) +- gitea: Gitea docs (unknown) +- kuma: Uptime Kuma (unknown) +- docker: Docker Engine (unknown) +- gitlab: GitLab CE (unknown) +- dozzle: Dozzle (unknown) +- filebrowser: Filebrowser (unknown) +- nodered: Node-RED (unknown) +- registry-docs: Docker Registry documentation (rolling) (unknown) +- htpasswd: Apache htpasswd (2.4) +- owasp-passwords: OWASP password storage (1a3f58ed2714f258a1d713966457f1a0f426e841) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; portainer, docker): Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure. +- portainer-ports (REASONED; portainer): UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute. +- portainer-setup (REASONED; portainer): Setup requires the logged setup_token; first user is admin with a password of at least 12 characters. +- portainer-auth (REASONED; portainer): Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider. +- portainer-socket (REASONED; portainer, docker): Mounted docker.sock grants host-root power; treat a Portainer admin accordingly. +- coolify-setup (REASONED; coolify): HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately. +- coolify-tls (REASONED; coolify): Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close. +- coolify-firewall (REASONED; coolify): Docker bypasses UFW; restrict dashboard ports at the cloud firewall. +- coolify-ssh (REASONED; coolify): Server SSH keys require no passphrase; stored key material is the whole secret. +- dokploy-ports (REASONED; dokploy): UI is 3000, Traefik 80/443; first setup creates admin. +- dokploy-tls (REASONED; dokploy): Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000. +- npm-ports (REASONED; npm): Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in. +- npm-admin (REASONED; npm): Change default first-run admin credentials before other work. +- vaultwarden-tls (REASONED; vaultwarden, vaultwarden-pin): Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com. +- vaultwarden-signup (REASONED; vaultwarden, vaultwarden-pin): SIGNUPS_ALLOWED defaults true; set false to close registration. +- vaultwarden-invites (REASONED; vaultwarden, vaultwarden-pin): INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains. +- vaultwarden-admin (REASONED; vaultwarden, vaultwarden-pin): ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$. +- vaultwarden-session (REASONED; vaultwarden, vaultwarden-pin): Admin sessions expire after 20 minutes by default. +- vaultwarden-port (REASONED; vaultwarden-pin): Container port 80 or non-Docker 8000; publish only to loopback/proxy network. +- dashboard-lifecycle (REASONED; dashboard): Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp. +- dashboard-forward (REASONED; dashboard): Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine. +- dashboard-token (REASONED; dashboard): Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only. +- dashboard-old-flags (REASONED; dashboard-old): Older 2.x enable-skip-login and enable-insecure-login default false; keep both false. +- dashboard-mfa (REASONED; dashboard): API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI. +- jenkins-auth (REASONED; jenkins): Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too. +- jenkins-matrix (REASONED; jenkins): Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control. +- jenkins-csrf (REASONED; jenkins): Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch. +- jenkins-agents (REASONED; jenkins): Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port. +- jenkins-mfa (REASONED; jenkins): No native second factor; enforce MFA through SSO/fronting identity. +- gitea-bind (REASONED; gitea-pin): HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy. +- gitea-tls (REASONED; gitea): Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE. +- gitea-registration (REASONED; gitea): Set service DISABLE_REGISTRATION=true; default false. +- gitea-private (REASONED; gitea): Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false. +- gitea-mfa (REASONED; gitea, gitea-mfa-min): Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later. +- gitea-tokens (REASONED; gitea): With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens. +- gitlab-root (REASONED; gitlab): Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG. +- gitlab-signup (REASONED; gitlab): signup_enabled defaults true; disable for a private forge. +- gitlab-approval (REASONED; gitlab): New instances require admin approval after signup by default; do not rely on approval alone. +- gitlab-visibility (REASONED; gitlab): Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants. +- gitlab-mfa (REASONED; gitlab): Enforce two-factor authentication for every user; available on all tiers. +- kuma-proxy (REASONED; kuma): Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required. +- kuma-admin (REASONED; kuma): Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public. +- docker-root (REASONED; docker): Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375. +- docker-ssh (REASONED; docker): DOCKER_HOST/context can select SSH to the Unix socket without a new listener. +- docker-mtls (REASONED; docker): 2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too. +- dozzle-socket (REASONED; dozzle, docker): Docker-socket access has host-level power; keep Dozzle private with fronting MFA. +- dozzle-auth (REASONED; dozzle): Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy. +- dozzle-actions (REASONED; dozzle): Leave optional container actions/shell off unless needed; either enables remote command execution. +- registry-auth (REASONED; registry-docs): registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution. +- registry-bcrypt (REASONED; registry-docs, htpasswd, owasp-passwords): Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ. +- registry-private (REASONED; registry-docs): Use private networking and tunnels/Access with fronting MFA. +- filebrowser-admin (REASONED; filebrowser): Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure. +- filebrowser-private (REASONED; filebrowser): Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA. +- nodered-default (REASONED; nodered): Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification. +- nodered-auth (REASONED; nodered): Set adminAuth with bcrypt passwords from node-red admin hash-pw. +- nodered-secret (REASONED; nodered): Set credentialSecret yourself; otherwise generated, and stored credentials depend on it. +- nodered-private (REASONED; nodered): Keep editor private with fronting MFA; it has the power of its flows. +- verify-listeners (REASONED; portainer, dokploy, npm, kuma, docker): Read every panel/API listener; expect loopback or absence, never wildcard exposure. +- verify-panel (REASONED; portainer, jenkins, gitea): Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded. +- verify-docker-plain (REASONED; docker): Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive. +- verify-docker-mtls (REASONED; docker): 2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200. +- verify-gitlab (REASONED; gitlab): Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "7cea8ce7dc2689ed90673f4d926b8259c0c8f0b24281b47ea184dfe91ffa1289", + "components": { + "portainer": { + "name": "Portainer", + "basis": "unknown", + "sources": { + "s3e163c7d3c38": "https://docs.portainer.io/start/install-ce/server/docker/linux", + "s115489d2f86f": "https://docs.portainer.io/start/install-ce/server/setup", + "s59297757a042": "https://docs.portainer.io/admin/settings/authentication", + "s8901bbe7c3d3": "https://docs.portainer.io/admin/settings/authentication/oauth" + } + }, + "dashboard-old": { + "name": "Dashboard arguments", + "basis": "v2.7.0", + "sources": { + "sacea2944651e": "https://github.com/kubernetes-retired/dashboard/blob/v2.7.0/docs/common/dashboard-arguments.md" + } + }, + "gitea-pin": { + "name": "Gitea listener", + "basis": "v1.27.3", + "sources": { + "s31dd622b1c12": "https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122" + } + }, + "gitea-mfa-min": { + "name": "Gitea MFA minimum", + "basis": "1.24", + "sources": { + "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet" + } + }, + "coolify": { + "name": "Coolify", + "basis": "unknown", + "sources": { + "s41e2d21202d9": "https://coolify.io/docs/start-with-self-hosted", + "sf2ae62425b35": "https://coolify.io/docs/core/infrastructure/servers/firewall", + "sf5d819d5d87d": "https://coolify.io/docs/core/networking/proxy/overview", + "s90addf9d8d5d": "https://coolify.io/docs/core/networking/dns", + "s683244a61c48": "https://coolify.io/docs/core/infrastructure/servers/openssh" + } + }, + "dokploy": { + "name": "Dokploy", + "basis": "unknown", + "sources": { + "sc2cc854c9b7e": "https://docs.dokploy.com/docs/core/installation" + } + }, + "npm": { + "name": "Nginx Proxy Manager", + "basis": "unknown", + "sources": { + "se553d384e783": "https://nginxproxymanager.com/setup/" + } + }, + "vaultwarden": { + "name": "Vaultwarden docs", + "basis": "unknown", + "sources": { + "s0bbe8ad856cd": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page", + "s6d3d0d02dcc9": "https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users", + "s60d064cae50e": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS" + } + }, + "vaultwarden-pin": { + "name": "Vaultwarden config", + "basis": "3347698712d3e99e652ad4394ef2fc0ce2800fdd", + "sources": { + "s31c5fc79f5a0": "https://github.com/dani-garcia/vaultwarden/blob/3347698712d3e99e652ad4394ef2fc0ce2800fdd/.env.template" + } + }, + "dashboard": { + "name": "Dashboard docs", + "basis": "unknown", + "sources": { + "s73a36a43f6b5": "https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/" + } + }, + "jenkins": { + "name": "Jenkins", + "basis": "unknown", + "sources": { + "s524c8d5871f1": "https://www.jenkins.io/doc/book/security/managing-security/", + "sdfee751aeeee": "https://www.jenkins.io/doc/book/security/access-control/", + "s6f5287fd36d4": "https://www.jenkins.io/doc/book/security/csrf-protection/" + } + }, + "gitea": { + "name": "Gitea docs", + "basis": "unknown", + "sources": { + "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet", + "s3b7060d58383": "https://docs.gitea.com/usage/user-setting/multi-factor-authentication/" + } + }, + "kuma": { + "name": "Uptime Kuma", + "basis": "unknown", + "sources": { + "s05b853ba300b": "https://github.com/louislam/uptime-kuma", + "sed52bfca4039": "https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" + } + }, + "docker": { + "name": "Docker Engine", + "basis": "unknown", + "sources": { + "sfe15a8156c18": "https://docs.docker.com/engine/security/protect-access/", + "s1b5ec54421b0": "https://docs.docker.com/engine/daemon/remote-access/" + } + }, + "gitlab": { + "name": "GitLab CE", + "basis": "unknown", + "sources": { + "s3b7ac561a92e": "https://docs.gitlab.com/install/docker/installation/", + "s6e8d16cad2ef": "https://docs.gitlab.com/administration/settings/sign_up_restrictions/", + "s6b8a68a77e17": "https://docs.gitlab.com/administration/settings/visibility_and_access_controls/", + "s0347014ce554": "https://docs.gitlab.com/security/two_factor_authentication/" + } + }, + "dozzle": { + "name": "Dozzle", + "basis": "unknown", + "sources": { + "s45b3eab2515b": "https://dozzle.dev/guide/authentication" + } + }, + "filebrowser": { + "name": "Filebrowser", + "basis": "unknown", + "sources": { + "s6386c9e2356b": "https://github.com/filebrowser/filebrowser" + } + }, + "nodered": { + "name": "Node-RED", + "basis": "unknown", + "sources": { + "sa94f329023fe": "https://nodered.org/docs/user-guide/runtime/securing-node-red" + } + }, + "registry-docs": { + "name": "Docker Registry documentation (rolling)", + "basis": "unknown", + "sources": { + "sa315b3f8bfa6": "https://distribution.github.io/distribution/about/deploying/" + } + }, + "htpasswd": { + "name": "Apache htpasswd", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "owasp-passwords": { + "name": "OWASP password storage", + "basis": "1a3f58ed2714f258a1d713966457f1a0f426e841", + "sources": { + "s0f70ec4aa271": "https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139" + } + } + }, + "claims": { + "private": {"text": "Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, + "portainer-ports": {"text": "UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute.", "components": ["portainer"], "sources": ["portainer:s3e163c7d3c38"], "status": "REASONED"}, + "portainer-setup": {"text": "Setup requires the logged setup_token; first user is admin with a password of at least 12 characters.", "components": ["portainer"], "sources": ["portainer:s115489d2f86f"], "status": "REASONED"}, + "portainer-auth": {"text": "Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider.", "components": ["portainer"], "sources": ["portainer:s59297757a042", "portainer:s8901bbe7c3d3"], "status": "REASONED"}, + "portainer-socket": {"text": "Mounted docker.sock grants host-root power; treat a Portainer admin accordingly.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, + "coolify-setup": {"text": "HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately.", "components": ["coolify"], "sources": ["coolify:s41e2d21202d9"], "status": "REASONED"}, + "coolify-tls": {"text": "Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35", "coolify:sf5d819d5d87d", "coolify:s90addf9d8d5d"], "status": "REASONED"}, + "coolify-firewall": {"text": "Docker bypasses UFW; restrict dashboard ports at the cloud firewall.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35"], "status": "REASONED"}, + "coolify-ssh": {"text": "Server SSH keys require no passphrase; stored key material is the whole secret.", "components": ["coolify"], "sources": ["coolify:s683244a61c48"], "status": "REASONED"}, + "dokploy-ports": {"text": "UI is 3000, Traefik 80/443; first setup creates admin.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, + "dokploy-tls": {"text": "Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, + "npm-ports": {"text": "Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, + "npm-admin": {"text": "Change default first-run admin credentials before other work.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, + "vaultwarden-tls": {"text": "Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s60d064cae50e", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-signup": {"text": "SIGNUPS_ALLOWED defaults true; set false to close registration.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-invites": {"text": "INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-admin": {"text": "ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-session": {"text": "Admin sessions expire after 20 minutes by default.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-port": {"text": "Container port 80 or non-Docker 8000; publish only to loopback/proxy network.", "components": ["vaultwarden-pin"], "sources": ["vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "dashboard-lifecycle": {"text": "Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-forward": {"text": "Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-token": {"text": "Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-old-flags": {"text": "Older 2.x enable-skip-login and enable-insecure-login default false; keep both false.", "components": ["dashboard-old"], "sources": ["dashboard-old:sacea2944651e"], "status": "REASONED"}, + "dashboard-mfa": {"text": "API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "jenkins-auth": {"text": "Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, + "jenkins-matrix": {"text": "Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control.", "components": ["jenkins"], "sources": ["jenkins:sdfee751aeeee"], "status": "REASONED"}, + "jenkins-csrf": {"text": "Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch.", "components": ["jenkins"], "sources": ["jenkins:s6f5287fd36d4"], "status": "REASONED"}, + "jenkins-agents": {"text": "Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1"], "status": "REASONED"}, + "jenkins-mfa": {"text": "No native second factor; enforce MFA through SSO/fronting identity.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, + "gitea-bind": {"text": "HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy.", "components": ["gitea-pin"], "sources": ["gitea-pin:s31dd622b1c12"], "status": "REASONED"}, + "gitea-tls": {"text": "Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-registration": {"text": "Set service DISABLE_REGISTRATION=true; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-private": {"text": "Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-mfa": {"text": "Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later.", "components": ["gitea", "gitea-mfa-min"], "sources": ["gitea:s57933519c308", "gitea:s3b7060d58383", "gitea-mfa-min:s57933519c308"], "status": "REASONED"}, + "gitea-tokens": {"text": "With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens.", "components": ["gitea"], "sources": ["gitea:s3b7060d58383"], "status": "REASONED"}, + "gitlab-root": {"text": "Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED"}, + "gitlab-signup": {"text": "signup_enabled defaults true; disable for a private forge.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, + "gitlab-approval": {"text": "New instances require admin approval after signup by default; do not rely on approval alone.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, + "gitlab-visibility": {"text": "Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants.", "components": ["gitlab"], "sources": ["gitlab:s6b8a68a77e17"], "status": "REASONED"}, + "gitlab-mfa": {"text": "Enforce two-factor authentication for every user; available on all tiers.", "components": ["gitlab"], "sources": ["gitlab:s0347014ce554"], "status": "REASONED"}, + "kuma-proxy": {"text": "Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b", "kuma:sed52bfca4039"], "status": "REASONED"}, + "kuma-admin": {"text": "Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b"], "status": "REASONED"}, + "docker-root": {"text": "Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375.", "components": ["docker"], "sources": ["docker:sfe15a8156c18", "docker:s1b5ec54421b0"], "status": "REASONED"}, + "docker-ssh": {"text": "DOCKER_HOST/context can select SSH to the Unix socket without a new listener.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, + "docker-mtls": {"text": "2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, + "dozzle-socket": {"text": "Docker-socket access has host-level power; keep Dozzle private with fronting MFA.", "components": ["dozzle", "docker"], "sources": ["dozzle:s45b3eab2515b", "docker:sfe15a8156c18"], "status": "REASONED"}, + "dozzle-auth": {"text": "Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, + "dozzle-actions": {"text": "Leave optional container actions/shell off unless needed; either enables remote command execution.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, + "registry-auth": {"text": "registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, + "registry-bcrypt": {"text": "Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ.", "components": ["registry-docs", "htpasswd", "owasp-passwords"], "sources": ["registry-docs:sa315b3f8bfa6", "htpasswd:scde9004bb967", "owasp-passwords:s0f70ec4aa271"], "status": "REASONED"}, + "registry-private": {"text": "Use private networking and tunnels/Access with fronting MFA.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, + "filebrowser-admin": {"text": "Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, + "filebrowser-private": {"text": "Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, + "nodered-default": {"text": "Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-auth": {"text": "Set adminAuth with bcrypt passwords from node-red admin hash-pw.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-secret": {"text": "Set credentialSecret yourself; otherwise generated, and stored credentials depend on it.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-private": {"text": "Keep editor private with fronting MFA; it has the power of its flows.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "verify-listeners": {"text": "Read every panel/API listener; expect loopback or absence, never wildcard exposure.", "components": ["portainer", "dokploy", "npm", "kuma", "docker"], "sources": ["portainer:s3e163c7d3c38", "dokploy:sc2cc854c9b7e", "npm:se553d384e783", "kuma:s05b853ba300b", "docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, + "verify-panel": {"text": "Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded.", "components": ["portainer", "jenkins", "gitea"], "sources": ["portainer:s3e163c7d3c38", "jenkins:sdfee751aeeee", "gitea:s57933519c308"], "status": "REASONED", "verify": [1]}, + "verify-docker-plain": {"text": "Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive.", "components": ["docker"], "sources": ["docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, + "verify-docker-mtls": {"text": "2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED", "verify": [1]}, + "verify-gitlab": {"text": "Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/dotnet.md b/dotnet.md index a7ac80a0d..0928ae317 100644 --- a/dotnet.md +++ b/dotnet.md @@ -1,318 +1,22 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "10f455bd5e1e9cc54ea69a2d50883c9cba79de0ada5085b56891741f8232ee5d", - "components": { - "docs": { - "name": "ASP.NET Core documentation", - "basis": ".NET 10", - "sources": { - "s8c762260991c": "https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-10.0", - "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", - "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", - "s3790c49f70b9": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-10.0", - "s9d10c561bb48": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions?view=aspnetcore-10.0", - "sa3c22a38f4cb": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-10.0", - "sb6780977fa3f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy?view=aspnetcore-10.0", - "s592dc85bb71f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0", - "s6afc0255098f": "https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.dangerousacceptanyservercertificatevalidator?view=net-10.0", - "s2d5a930aac35": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-10.0" - } - }, - "rate-min": { - "name": "ASP.NET Core rate-limiter minimum", - "basis": ".NET 7", - "sources": { - "sd3fef8f45066": "https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit" - } - }, - "net10": { - "name": "ASP.NET Core", - "basis": ".NET 10", - "sources": { - "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", - "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", - "s0d7c367ac06f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-10.0", - "sc0654e58fc4c": "https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-10.0", - "s837df4b1657f": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints?view=aspnetcore-10.0", - "s2f7e6a8213f1": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-10.0", - "s61afb89034f8": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0", - "s9d290374f4e8": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint?view=aspnetcore-10.0", - "s1ce47370d948": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-10.0", - "s55f298bf0048": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.disablerequestsizelimitattribute?view=aspnetcore-10.0", - "s792d3e8f3e42": "https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-10.0", - "s529a3d7cce08": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0", - "s6682926c8da7": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-10.0", - "s66f1ab4555d6": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-10.0", - "sd635cefc0a9f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate?view=aspnetcore-10.0", - "s9463626a3d0e": "https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-10.0", - "s1d58219cedec": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0", - "s1f9d4c5e975c": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0", - "sc41b77cc06c8": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0", - "se101626c29d6": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0" - } - }, - "source": { - "name": "ASP.NET Core source", - "basis": "v10.0.0", - "sources": { - "sb62cff70e3b8": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpsPolicy/src/HstsOptions.cs", - "sfc86e3f62243": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs", - "s903da00d32b5": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs", - "sc00d8d67e93b": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/Http2Limits.cs", - "s67f347ba7947": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Http/Http/src/Features/FormOptions.cs", - "s18c44d6c2fb7": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs" - } - }, - "proxy-fix": { - "name": "ASP.NET Core proxy hardening", - "basis": "8.0.17", - "sources": { - "s2864662bcd21": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/8/forwarded-headers-unknown-proxies?view=aspnetcore-10.0" - } - }, - "runtime": { - "name": "NET runtime source", - "basis": "v10.0.0", - "sources": { - "s29ee940c94b9": "https://raw.githubusercontent.com/dotnet/runtime/v10.0.0/src/native/libs/System.Security.Cryptography.Native/pal_x509_root.c" - } - }, - "openssl": { - "name": "OpenSSL documentation", - "basis": "3.0", - "sources": { - "scac6dada5f6a": "https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/" - } - }, - "precedence": { - "name": "WebApplicationBuilder precedence", - "basis": ".NET 7", - "sources": { - "s10d46bc97fef": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/7/environment-variable-precedence?view=aspnetcore-10.0" - } - }, - "iis": { - "name": "IIS documentation", - "basis": "IIS 10", - "sources": { - "sb2eac755b583": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/" - } - }, - "iis-rolling": { - "name": "IIS documentation (rolling)", - "basis": "unknown", - "sources": { - "s267eddd43502": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/httpprotocol/customheaders/" - } - }, - "dotnet-rolling": { - "name": ".NET documentation (rolling)", - "basis": "unknown", - "sources": { - "s640872a17a8a": "https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography" - } - }, - "cf-docs": { - "name": "Cloudflare 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", - "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", - "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", - "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" - } - } - }, - "claims": { - "tls": {"text": "Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED"}, - "tls-protocols": {"text": "Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3.", "components": ["net10"], "sources": ["net10:s74dcd6a72d63"], "status": "REASONED"}, - "hsts-age": {"text": "NET 10 HSTS defaults to 30 days; example selects 365 days outside Development.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, - "hsts-subdomains": {"text": "HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, - "hsts-preload": {"text": "HSTS Preload defaults false.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, - "redirect": {"text": "UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED"}, - "proxy-bind": {"text": "ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s5701d26de241"], "status": "REASONED"}, - "proxy-default": {"text": "NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, - "proxy-networks": {"text": "NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, - "proxy-hardening": {"text": "Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy.", "components": ["net10", "proxy-fix"], "sources": ["net10:s5701d26de241", "proxy-fix:s2864662bcd21"], "status": "REASONED"}, - "proxy-https": {"text": "When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops.", "components": ["docs"], "sources": ["docs:s8c762260991c", "docs:s5701d26de241"], "status": "REASONED"}, - "password": {"text": "Identity uses PBKDF2 with default 100000 iterations; keep its password hasher.", "components": ["docs"], "sources": ["docs:s3790c49f70b9", "docs:s9d10c561bb48"], "status": "REASONED"}, - "lockout": {"text": "Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false.", "components": ["net10"], "sources": ["net10:s0d7c367ac06f"], "status": "REASONED"}, - "cookies": {"text": "Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options.", "components": ["docs", "net10"], "sources": ["docs:sa3c22a38f4cb", "docs:sb6780977fa3f", "net10:s0d7c367ac06f"], "status": "REASONED"}, - "samesite": {"text": "NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, - "remote-cookies": {"text": "Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, - "authorization": {"text": "Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages.", "components": ["docs"], "sources": ["docs:sa3c22a38f4cb"], "status": "REASONED"}, - "login-limit": {"text": "NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing.", "components": ["rate-min"], "sources": ["rate-min:sd3fef8f45066"], "status": "REASONED"}, - "oidc": {"text": "Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json.", "components": ["docs"], "sources": ["docs:s592dc85bb71f"], "status": "REASONED"}, - "mfa": {"text": "Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation.", "components": ["docs", "cf-docs"], "sources": ["docs:s592dc85bb71f", "docs:s2d5a930aac35", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "cf-docs:sfddb92e438ca", "cf-docs:s863803bd9a1f"], "status": "REASONED"}, - "client-validation": {"text": "Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback.", "components": ["docs"], "sources": ["docs:s6afc0255098f"], "status": "REASONED"}, - "client-ca": {"text": "On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots.", "components": ["runtime", "openssl", "dotnet-rolling"], "sources": ["dotnet-rolling:s640872a17a8a", "runtime:s29ee940c94b9", "openssl:scac6dada5f6a"], "status": "REASONED"}, - "developer-errors": {"text": "Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s61afb89034f8"], "status": "REASONED"}, - "error-handler": {"text": "Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED"}, - "environment": {"text": "Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs.", "components": ["net10", "precedence"], "sources": ["net10:s61afb89034f8", "precedence:s10d46bc97fef"], "status": "REASONED"}, - "database-diagnostics": {"text": "Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s9d290374f4e8"], "status": "REASONED"}, - "body-limit": {"text": "NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "body-override": {"text": "RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s55f298bf0048"], "status": "REASONED"}, - "multipart": {"text": "MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413.", "components": ["source", "net10"], "sources": ["source:s67f347ba7947", "net10:s792d3e8f3e42"], "status": "REASONED"}, - "iis-body": {"text": "In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s792d3e8f3e42", "net10:s529a3d7cce08"], "status": "REASONED"}, - "keepalive": {"text": "Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "header-timeout": {"text": "Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "connections": {"text": "MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "upgraded": {"text": "Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "header-count": {"text": "Kestrel MaxRequestHeaderCount defaults 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "header-size": {"text": "Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "request-rate": {"text": "MinRequestBodyDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "response-rate": {"text": "MinResponseDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "rate-scope": {"text": "HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED"}, - "h2-streams": {"text": "NET 10 HTTP/2 MaxStreamsPerConnection defaults 100.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-table": {"text": "NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-frame": {"text": "NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-header": {"text": "NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-connection-window": {"text": "NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-stream-window": {"text": "NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-ping-delay": {"text": "NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-ping-timeout": {"text": "NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-scope": {"text": "Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:sc00d8d67e93b"], "status": "REASONED"}, - "key-defaults": {"text": "Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys.", "components": ["net10"], "sources": ["net10:s66f1ab4555d6"], "status": "REASONED"}, - "key-persistence": {"text": "Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, - "key-encryption": {"text": "Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, - "key-isolation": {"text": "Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys.", "components": ["net10"], "sources": ["net10:s6682926c8da7"], "status": "REASONED"}, - "cookie-sharing": {"text": "Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration.", "components": ["net10"], "sources": ["net10:s9463626a3d0e"], "status": "REASONED"}, - "server-header": {"text": "NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access.", "components": ["source"], "sources": ["source:s18c44d6c2fb7"], "status": "REASONED"}, - "iis-headers": {"text": "Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later.", "components": ["iis", "iis-rolling"], "sources": ["iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED"}, - "openapi": {"text": "NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy.", "components": ["net10"], "sources": ["net10:s1d58219cedec"], "status": "REASONED"}, - "swagger": {"text": "Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document.", "components": ["net10"], "sources": ["net10:s1f9d4c5e975c"], "status": "REASONED"}, - "health": {"text": "Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, - "health-host": {"text": "RequireHost checks a spoofable Host header and is not a network or authentication boundary.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, - "host-filter": {"text": "AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate.", "components": ["net10"], "sources": ["net10:s5701d26de241", "net10:se101626c29d6"], "status": "REASONED"}, - "verify-redirect": {"text": "HTTP should return 307/308 with HTTPS Location.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED", "verify": [1]}, - "verify-tls": {"text": "HTTPS must validate without -k and show HSTS.", "components": ["docs", "net10"], "sources": ["docs:s8c762260991c", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, - "verify-api": {"text": "Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first.", "components": ["net10"], "sources": ["net10:s837df4b1657f"], "status": "REASONED", "verify": [1]}, - "verify-bind": {"text": "Inspect every listener for 127.0.0.1 and ::1 behind the proxy.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, - "verify-errors": {"text": "Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED", "verify": [2]}, - "verify-body": {"text": "Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED", "verify": [3]}, - "verify-aux": {"text": "Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization.", "components": ["net10"], "sources": ["net10:s1d58219cedec", "net10:s1f9d4c5e975c", "net10:sc41b77cc06c8"], "status": "REASONED", "verify": [4]}, - "verify-migrations": {"text": "GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure.", "components": ["net10"], "sources": ["net10:s9d290374f4e8"], "status": "REASONED"}, - "verify-headers": {"text": "Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported.", "components": ["source", "iis", "iis-rolling"], "sources": ["source:s18c44d6c2fb7", "iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED", "verify": [5]}, - "verify-cookie": {"text": "Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes.", "components": ["net10"], "sources": ["net10:s837df4b1657f", "net10:s66f1ab4555d6", "net10:s9463626a3d0e"], "status": "REASONED", "verify": [6]}, - "verify-antiforgery": {"text": "Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:s66f1ab4555d6"], "status": "REASONED"} - } -} ---- # ASP.NET Core and Kestrel: TLS and authentication - -**Version basis** +Written against ASP.NET Core documentation .NET 10, ASP.NET Core rate-limiter minimum .NET 7, ASP.NET Core .NET 10, ASP.NET Core source v10.0.0, ASP.NET Core proxy hardening 8.0.17, NET runtime source v10.0.0, WebApplicationBuilder precedence .NET 7, and OpenSSL documentation 3.0, plus 1 more recorded basis; 1 further source is pinned to a commit and 2 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 71 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-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. +Preferred production layout: bind Kestrel to loopback and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). Kestrel can also terminate TLS itself, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). -Components (recorded basis): +## 1. HTTPS directly in Kestrel -- docs: ASP.NET Core documentation (.NET 10) -- rate-min: ASP.NET Core rate-limiter minimum (.NET 7) -- net10: ASP.NET Core (.NET 10) -- source: ASP.NET Core source (v10.0.0) -- proxy-fix: ASP.NET Core proxy hardening (8.0.17) -- runtime: NET runtime source (v10.0.0) -- openssl: OpenSSL documentation (3.0) -- precedence: WebApplicationBuilder precedence (.NET 7) -- iis: IIS documentation (IIS 10) -- iis-rolling: IIS documentation (rolling) (unknown) -- dotnet-rolling: .NET documentation (rolling) (unknown) -- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +`appsettings.json` with a PKCS12 file (for PEM files, `Path` is the certificate and `KeyPath` the private key; the docs warn against a plaintext `Password` here, so supply it from the environment or a secret store): -Claims (full source mapping in the front matter): - -- tls (REASONED; docs, net10): Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store. -- tls-protocols (REASONED; net10): Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3. -- hsts-age (REASONED; source): NET 10 HSTS defaults to 30 days; example selects 365 days outside Development. -- hsts-subdomains (REASONED; source): HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS. -- hsts-preload (REASONED; source): HSTS Preload defaults false. -- redirect (REASONED; docs): UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort. -- proxy-bind (REASONED; docs, net10): ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first. -- proxy-default (REASONED; net10, source): NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list. -- proxy-networks (REASONED; net10, source): NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty. -- proxy-hardening (REASONED; net10, proxy-fix): Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy. -- proxy-https (REASONED; docs): When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops. -- password (REASONED; docs): Identity uses PBKDF2 with default 100000 iterations; keep its password hasher. -- lockout (REASONED; net10): Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false. -- cookies (REASONED; docs, net10): Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options. -- samesite (REASONED; net10): NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks. -- remote-cookies (REASONED; net10): Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting. -- authorization (REASONED; docs): Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages. -- login-limit (REASONED; rate-min): NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing. -- oidc (REASONED; docs): Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json. -- mfa (REASONED; docs, cf-docs): Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation. -- client-validation (REASONED; docs): Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback. -- client-ca (REASONED; runtime, openssl, dotnet-rolling): On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots. -- developer-errors (REASONED; net10): Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment. -- error-handler (REASONED; net10): Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses. -- environment (REASONED; net10, precedence): Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs. -- database-diagnostics (REASONED; net10): Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes. -- body-limit (REASONED; net10, source): NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes. -- body-override (REASONED; net10): RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection. -- multipart (REASONED; source, net10): MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413. -- iis-body (REASONED; net10): In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting. -- keepalive (REASONED; net10, source): Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline. -- header-timeout (REASONED; net10, source): Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline. -- connections (REASONED; source): MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100. -- upgraded (REASONED; net10, source): Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null. -- header-count (REASONED; source): Kestrel MaxRequestHeaderCount defaults 100. -- header-size (REASONED; source): Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes. -- request-rate (REASONED; source): MinRequestBodyDataRate defaults 240 bytes/second with five-second grace. -- response-rate (REASONED; source): MinResponseDataRate defaults 240 bytes/second with five-second grace. -- rate-scope (REASONED; net10): HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger. -- h2-streams (REASONED; source): NET 10 HTTP/2 MaxStreamsPerConnection defaults 100. -- h2-table (REASONED; source): NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes. -- h2-frame (REASONED; source): NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes. -- h2-header (REASONED; source): NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192. -- h2-connection-window (REASONED; source): NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072. -- h2-stream-window (REASONED; source): NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304. -- h2-ping-delay (REASONED; source): NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings. -- h2-ping-timeout (REASONED; source): NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds. -- h2-scope (REASONED; net10, source): Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3. -- key-defaults (REASONED; net10): Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys. -- key-persistence (REASONED; net10): Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names. -- key-encryption (REASONED; net10): Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates. -- key-isolation (REASONED; net10): Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys. -- cookie-sharing (REASONED; net10): Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration. -- server-header (REASONED; source): NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access. -- iis-headers (REASONED; iis, iis-rolling): Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later. -- openapi (REASONED; net10): NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy. -- swagger (REASONED; net10): Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document. -- health (REASONED; net10): Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route. -- health-host (REASONED; net10): RequireHost checks a spoofable Host header and is not a network or authentication boundary. -- host-filter (REASONED; net10): AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate. -- verify-redirect (REASONED; docs): HTTP should return 307/308 with HTTPS Location. -- verify-tls (REASONED; docs, net10): HTTPS must validate without -k and show HSTS. -- verify-api (REASONED; net10): Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first. -- verify-bind (REASONED; docs, net10): Inspect every listener for 127.0.0.1 and ::1 behind the proxy. -- verify-errors (REASONED; net10): Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution. -- verify-body (REASONED; net10): Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection. -- verify-aux (REASONED; net10): Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization. -- verify-migrations (REASONED; net10): GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure. -- verify-headers (REASONED; source, iis, iis-rolling): Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported. -- verify-cookie (REASONED; net10): Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes. -- verify-antiforgery (REASONED; net10): Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery. - - -Preferred production layout: bind Kestrel to loopback and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). Kestrel can also terminate TLS itself, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). - -## 1. HTTPS directly in Kestrel - -`appsettings.json` with a PKCS12 file (for PEM files, `Path` is the certificate and `KeyPath` the private key; the docs warn against a plaintext `Password` here, so supply it from the environment or a secret store): - -```json -{ "Kestrel": { "Endpoints": { - "Http": { "Url": "http://*:80" }, - "Https": { "Url": "https://*:443", - "Certificate": { "Path": "/etc/ssl/private/server.pfx", "Password": "REPLACE_WITH_LONG_RANDOM_VALUE" } } -} } } -``` +```json +{ "Kestrel": { "Endpoints": { + "Http": { "Url": "http://*:80" }, + "Https": { "Url": "https://*:443", + "Certificate": { "Path": "/etc/ssl/private/server.pfx", "Password": "REPLACE_WITH_LONG_RANDOM_VALUE" } } +} } } +``` ```csharp builder.WebHost.ConfigureKestrel(serverOptions => @@ -752,3 +456,325 @@ Also retain an antiforgery token and its accompanying cookie across a restart, t - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, independent MFA and supported TOTP/WebAuthn factors (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24 - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, self-hosted public application authentication layer (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#L14-L16 - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, publicly routable origin protection requirement (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#L48-L48 + + + +## 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/dotnet.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): + +- docs: ASP.NET Core documentation (.NET 10) +- rate-min: ASP.NET Core rate-limiter minimum (.NET 7) +- net10: ASP.NET Core (.NET 10) +- source: ASP.NET Core source (v10.0.0) +- proxy-fix: ASP.NET Core proxy hardening (8.0.17) +- runtime: NET runtime source (v10.0.0) +- precedence: WebApplicationBuilder precedence (.NET 7) +- dotnet-rolling: .NET documentation (rolling) (unknown) +- openssl: OpenSSL documentation (3.0) +- iis: IIS documentation (IIS 10) +- iis-rolling: IIS documentation (rolling) (unknown) +- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) + +Claims (full source mapping in the JSON block below): + +- tls (REASONED; docs, net10): Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store. +- tls-protocols (REASONED; net10): Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3. +- hsts-age (REASONED; source): NET 10 HSTS defaults to 30 days; example selects 365 days outside Development. +- hsts-subdomains (REASONED; source): HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS. +- hsts-preload (REASONED; source): HSTS Preload defaults false. +- redirect (REASONED; docs): UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort. +- proxy-bind (REASONED; docs, net10): ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first. +- proxy-default (REASONED; net10, source): NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list. +- proxy-networks (REASONED; net10, source): NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty. +- proxy-hardening (REASONED; net10, proxy-fix): Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy. +- proxy-https (REASONED; docs): When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops. +- password (REASONED; docs): Identity uses PBKDF2 with default 100000 iterations; keep its password hasher. +- lockout (REASONED; net10): Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false. +- cookies (REASONED; docs, net10): Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options. +- samesite (REASONED; net10): NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks. +- remote-cookies (REASONED; net10): Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting. +- authorization (REASONED; docs): Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages. +- login-limit (REASONED; rate-min): NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing. +- oidc (REASONED; docs): Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json. +- mfa (REASONED; docs, cf-docs): Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation. +- client-validation (REASONED; docs): Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback. +- client-ca (REASONED; runtime, openssl, dotnet-rolling): On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots. +- developer-errors (REASONED; net10): Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment. +- error-handler (REASONED; net10): Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses. +- environment (REASONED; net10, precedence): Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs. +- database-diagnostics (REASONED; net10): Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes. +- body-limit (REASONED; net10, source): NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes. +- body-override (REASONED; net10): RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection. +- multipart (REASONED; source, net10): MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413. +- iis-body (REASONED; net10): In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting. +- keepalive (REASONED; net10, source): Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline. +- header-timeout (REASONED; net10, source): Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline. +- connections (REASONED; source): MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100. +- upgraded (REASONED; net10, source): Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null. +- header-count (REASONED; source): Kestrel MaxRequestHeaderCount defaults 100. +- header-size (REASONED; source): Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes. +- request-rate (REASONED; source): MinRequestBodyDataRate defaults 240 bytes/second with five-second grace. +- response-rate (REASONED; source): MinResponseDataRate defaults 240 bytes/second with five-second grace. +- rate-scope (REASONED; net10): HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger. +- h2-streams (REASONED; source): NET 10 HTTP/2 MaxStreamsPerConnection defaults 100. +- h2-table (REASONED; source): NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes. +- h2-frame (REASONED; source): NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes. +- h2-header (REASONED; source): NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192. +- h2-connection-window (REASONED; source): NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072. +- h2-stream-window (REASONED; source): NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304. +- h2-ping-delay (REASONED; source): NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings. +- h2-ping-timeout (REASONED; source): NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds. +- h2-scope (REASONED; net10, source): Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3. +- key-defaults (REASONED; net10): Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys. +- key-persistence (REASONED; net10): Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names. +- key-encryption (REASONED; net10): Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates. +- key-isolation (REASONED; net10): Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys. +- cookie-sharing (REASONED; net10): Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration. +- server-header (REASONED; source): NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access. +- iis-headers (REASONED; iis, iis-rolling): Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later. +- openapi (REASONED; net10): NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy. +- swagger (REASONED; net10): Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document. +- health (REASONED; net10): Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route. +- health-host (REASONED; net10): RequireHost checks a spoofable Host header and is not a network or authentication boundary. +- host-filter (REASONED; net10): AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate. +- verify-redirect (REASONED; docs): HTTP should return 307/308 with HTTPS Location. +- verify-tls (REASONED; docs, net10): HTTPS must validate without -k and show HSTS. +- verify-api (REASONED; net10): Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first. +- verify-bind (REASONED; docs, net10): Inspect every listener for 127.0.0.1 and ::1 behind the proxy. +- verify-errors (REASONED; net10): Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution. +- verify-body (REASONED; net10): Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection. +- verify-aux (REASONED; net10): Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization. +- verify-migrations (REASONED; net10): GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure. +- verify-headers (REASONED; source, iis, iis-rolling): Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported. +- verify-cookie (REASONED; net10): Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes. +- verify-antiforgery (REASONED; net10): Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "10f455bd5e1e9cc54ea69a2d50883c9cba79de0ada5085b56891741f8232ee5d", + "components": { + "docs": { + "name": "ASP.NET Core documentation", + "basis": ".NET 10", + "sources": { + "s8c762260991c": "https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-10.0", + "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", + "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", + "s3790c49f70b9": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-10.0", + "s9d10c561bb48": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions?view=aspnetcore-10.0", + "sa3c22a38f4cb": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-10.0", + "sb6780977fa3f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy?view=aspnetcore-10.0", + "s592dc85bb71f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0", + "s6afc0255098f": "https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.dangerousacceptanyservercertificatevalidator?view=net-10.0", + "s2d5a930aac35": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-10.0" + } + }, + "rate-min": { + "name": "ASP.NET Core rate-limiter minimum", + "basis": ".NET 7", + "sources": { + "sd3fef8f45066": "https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit" + } + }, + "net10": { + "name": "ASP.NET Core", + "basis": ".NET 10", + "sources": { + "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", + "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", + "s0d7c367ac06f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-10.0", + "sc0654e58fc4c": "https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-10.0", + "s837df4b1657f": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints?view=aspnetcore-10.0", + "s2f7e6a8213f1": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-10.0", + "s61afb89034f8": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0", + "s9d290374f4e8": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint?view=aspnetcore-10.0", + "s1ce47370d948": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-10.0", + "s55f298bf0048": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.disablerequestsizelimitattribute?view=aspnetcore-10.0", + "s792d3e8f3e42": "https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-10.0", + "s529a3d7cce08": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0", + "s6682926c8da7": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-10.0", + "s66f1ab4555d6": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-10.0", + "sd635cefc0a9f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate?view=aspnetcore-10.0", + "s9463626a3d0e": "https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-10.0", + "s1d58219cedec": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0", + "s1f9d4c5e975c": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0", + "sc41b77cc06c8": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0", + "se101626c29d6": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0" + } + }, + "source": { + "name": "ASP.NET Core source", + "basis": "v10.0.0", + "sources": { + "sb62cff70e3b8": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpsPolicy/src/HstsOptions.cs", + "sfc86e3f62243": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs", + "s903da00d32b5": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs", + "sc00d8d67e93b": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/Http2Limits.cs", + "s67f347ba7947": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Http/Http/src/Features/FormOptions.cs", + "s18c44d6c2fb7": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs" + } + }, + "proxy-fix": { + "name": "ASP.NET Core proxy hardening", + "basis": "8.0.17", + "sources": { + "s2864662bcd21": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/8/forwarded-headers-unknown-proxies?view=aspnetcore-10.0" + } + }, + "runtime": { + "name": "NET runtime source", + "basis": "v10.0.0", + "sources": { + "s29ee940c94b9": "https://raw.githubusercontent.com/dotnet/runtime/v10.0.0/src/native/libs/System.Security.Cryptography.Native/pal_x509_root.c" + } + }, + "precedence": { + "name": "WebApplicationBuilder precedence", + "basis": ".NET 7", + "sources": { + "s10d46bc97fef": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/7/environment-variable-precedence?view=aspnetcore-10.0" + } + }, + "dotnet-rolling": { + "name": ".NET documentation (rolling)", + "basis": "unknown", + "sources": { + "s640872a17a8a": "https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "3.0", + "sources": { + "scac6dada5f6a": "https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/" + } + }, + "iis": { + "name": "IIS documentation", + "basis": "IIS 10", + "sources": { + "sb2eac755b583": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/" + } + }, + "iis-rolling": { + "name": "IIS documentation (rolling)", + "basis": "unknown", + "sources": { + "s267eddd43502": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/httpprotocol/customheaders/" + } + }, + "cf-docs": { + "name": "Cloudflare 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", + "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", + "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", + "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" + } + } + }, + "claims": { + "tls": {"text": "Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED"}, + "tls-protocols": {"text": "Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3.", "components": ["net10"], "sources": ["net10:s74dcd6a72d63"], "status": "REASONED"}, + "hsts-age": {"text": "NET 10 HSTS defaults to 30 days; example selects 365 days outside Development.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "hsts-subdomains": {"text": "HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "hsts-preload": {"text": "HSTS Preload defaults false.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "redirect": {"text": "UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED"}, + "proxy-bind": {"text": "ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s5701d26de241"], "status": "REASONED"}, + "proxy-default": {"text": "NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, + "proxy-networks": {"text": "NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, + "proxy-hardening": {"text": "Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy.", "components": ["net10", "proxy-fix"], "sources": ["net10:s5701d26de241", "proxy-fix:s2864662bcd21"], "status": "REASONED"}, + "proxy-https": {"text": "When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops.", "components": ["docs"], "sources": ["docs:s8c762260991c", "docs:s5701d26de241"], "status": "REASONED"}, + "password": {"text": "Identity uses PBKDF2 with default 100000 iterations; keep its password hasher.", "components": ["docs"], "sources": ["docs:s3790c49f70b9", "docs:s9d10c561bb48"], "status": "REASONED"}, + "lockout": {"text": "Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false.", "components": ["net10"], "sources": ["net10:s0d7c367ac06f"], "status": "REASONED"}, + "cookies": {"text": "Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options.", "components": ["docs", "net10"], "sources": ["docs:sa3c22a38f4cb", "docs:sb6780977fa3f", "net10:s0d7c367ac06f"], "status": "REASONED"}, + "samesite": {"text": "NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, + "remote-cookies": {"text": "Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, + "authorization": {"text": "Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages.", "components": ["docs"], "sources": ["docs:sa3c22a38f4cb"], "status": "REASONED"}, + "login-limit": {"text": "NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing.", "components": ["rate-min"], "sources": ["rate-min:sd3fef8f45066"], "status": "REASONED"}, + "oidc": {"text": "Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json.", "components": ["docs"], "sources": ["docs:s592dc85bb71f"], "status": "REASONED"}, + "mfa": {"text": "Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation.", "components": ["docs", "cf-docs"], "sources": ["docs:s592dc85bb71f", "docs:s2d5a930aac35", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "cf-docs:sfddb92e438ca", "cf-docs:s863803bd9a1f"], "status": "REASONED"}, + "client-validation": {"text": "Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback.", "components": ["docs"], "sources": ["docs:s6afc0255098f"], "status": "REASONED"}, + "client-ca": {"text": "On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots.", "components": ["runtime", "openssl", "dotnet-rolling"], "sources": ["dotnet-rolling:s640872a17a8a", "runtime:s29ee940c94b9", "openssl:scac6dada5f6a"], "status": "REASONED"}, + "developer-errors": {"text": "Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s61afb89034f8"], "status": "REASONED"}, + "error-handler": {"text": "Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED"}, + "environment": {"text": "Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs.", "components": ["net10", "precedence"], "sources": ["net10:s61afb89034f8", "precedence:s10d46bc97fef"], "status": "REASONED"}, + "database-diagnostics": {"text": "Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s9d290374f4e8"], "status": "REASONED"}, + "body-limit": {"text": "NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "body-override": {"text": "RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s55f298bf0048"], "status": "REASONED"}, + "multipart": {"text": "MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413.", "components": ["source", "net10"], "sources": ["source:s67f347ba7947", "net10:s792d3e8f3e42"], "status": "REASONED"}, + "iis-body": {"text": "In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s792d3e8f3e42", "net10:s529a3d7cce08"], "status": "REASONED"}, + "keepalive": {"text": "Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "header-timeout": {"text": "Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "connections": {"text": "MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "upgraded": {"text": "Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "header-count": {"text": "Kestrel MaxRequestHeaderCount defaults 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "header-size": {"text": "Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "request-rate": {"text": "MinRequestBodyDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "response-rate": {"text": "MinResponseDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "rate-scope": {"text": "HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED"}, + "h2-streams": {"text": "NET 10 HTTP/2 MaxStreamsPerConnection defaults 100.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-table": {"text": "NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-frame": {"text": "NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-header": {"text": "NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-connection-window": {"text": "NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-stream-window": {"text": "NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-ping-delay": {"text": "NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-ping-timeout": {"text": "NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-scope": {"text": "Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:sc00d8d67e93b"], "status": "REASONED"}, + "key-defaults": {"text": "Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys.", "components": ["net10"], "sources": ["net10:s66f1ab4555d6"], "status": "REASONED"}, + "key-persistence": {"text": "Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, + "key-encryption": {"text": "Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, + "key-isolation": {"text": "Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys.", "components": ["net10"], "sources": ["net10:s6682926c8da7"], "status": "REASONED"}, + "cookie-sharing": {"text": "Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration.", "components": ["net10"], "sources": ["net10:s9463626a3d0e"], "status": "REASONED"}, + "server-header": {"text": "NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access.", "components": ["source"], "sources": ["source:s18c44d6c2fb7"], "status": "REASONED"}, + "iis-headers": {"text": "Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later.", "components": ["iis", "iis-rolling"], "sources": ["iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED"}, + "openapi": {"text": "NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy.", "components": ["net10"], "sources": ["net10:s1d58219cedec"], "status": "REASONED"}, + "swagger": {"text": "Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document.", "components": ["net10"], "sources": ["net10:s1f9d4c5e975c"], "status": "REASONED"}, + "health": {"text": "Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, + "health-host": {"text": "RequireHost checks a spoofable Host header and is not a network or authentication boundary.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, + "host-filter": {"text": "AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate.", "components": ["net10"], "sources": ["net10:s5701d26de241", "net10:se101626c29d6"], "status": "REASONED"}, + "verify-redirect": {"text": "HTTP should return 307/308 with HTTPS Location.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "HTTPS must validate without -k and show HSTS.", "components": ["docs", "net10"], "sources": ["docs:s8c762260991c", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, + "verify-api": {"text": "Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first.", "components": ["net10"], "sources": ["net10:s837df4b1657f"], "status": "REASONED", "verify": [1]}, + "verify-bind": {"text": "Inspect every listener for 127.0.0.1 and ::1 behind the proxy.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, + "verify-errors": {"text": "Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED", "verify": [2]}, + "verify-body": {"text": "Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED", "verify": [3]}, + "verify-aux": {"text": "Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization.", "components": ["net10"], "sources": ["net10:s1d58219cedec", "net10:s1f9d4c5e975c", "net10:sc41b77cc06c8"], "status": "REASONED", "verify": [4]}, + "verify-migrations": {"text": "GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure.", "components": ["net10"], "sources": ["net10:s9d290374f4e8"], "status": "REASONED"}, + "verify-headers": {"text": "Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported.", "components": ["source", "iis", "iis-rolling"], "sources": ["source:s18c44d6c2fb7", "iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED", "verify": [5]}, + "verify-cookie": {"text": "Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes.", "components": ["net10"], "sources": ["net10:s837df4b1657f", "net10:s66f1ab4555d6", "net10:s9463626a3d0e"], "status": "REASONED", "verify": [6]}, + "verify-antiforgery": {"text": "Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:s66f1ab4555d6"], "status": "REASONED"} + } +} +```` + diff --git a/frontend-frameworks.md b/frontend-frameworks.md index 810eed667..7c62ee6d1 100644 --- a/frontend-frameworks.md +++ b/frontend-frameworks.md @@ -1,226 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "c967988728d40cccfa0b3e543320cd6e6d4f7568099a9805fb91a941243d4c1d", - "components": { - "adapter": { - "name": "SvelteKit adapter-node", - "basis": "@sveltejs/adapter-node@5.5.7", - "sources": { - "s011f4e44c91a": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50", - "s93f13e6b8d31": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12", - "sd23cac9a730f": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60", - "s200d6e800c74": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L24-L38", - "sf17c5646623c": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L118", - "s1ffeda9bbb37": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L69", - "s8d4fa8e7426b": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L243", - "sfd51397a510e": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L17-L24", - "s6152d66c6b19": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L95-L105", - "s7b3a11637166": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L209-L239" - } - }, - "svelte": { - "name": "SvelteKit documentation", - "basis": "unknown", - "sources": { - "sc183fdb68165": "https://svelte.dev/docs/kit/$env-static-public", - "s59abf03bae6a": "https://svelte.dev/docs/kit/adapter-node", - "s329dda89ba78": "https://svelte.dev/docs/kit/configuration", - "se2fdc4fd5885": "https://svelte.dev/docs/kit/server-only-modules" - } - }, - "svelte-kit-pin": { - "name": "SvelteKit routing and hooks documentation", - "basis": "@sveltejs/kit@2.70.3", - "sources": { - "sca07fbe19546": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/10-routing.md#L291-L332", - "sa9bbf285a91c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/20-load.md#L702-L714", - "s380cc33507e0": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/30-form-actions.md#L1-L5", - "sf5b873616d3c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/30-advanced/20-hooks.md#L15-L36", - "s5ad7a525e0ea": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/60-remote-functions.md#L1300-L1303", - "s6a78ad19d884": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L70-L78", - "s0969ea98a4f1": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L178-L250", - "sffcf647d5328": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/data/index.js#L39-L97", - "sfb110948d2ca": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/client/client.js#L1217-L1235", - "s67fe32e7a18d": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L73-L106", - "sc4708064d4fd": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L111-L130", - "s95d5c7dd7a50": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L333-L343", - "sca363a35f3fc": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L456-L498", - "s89773d0c39e7": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L274-L306", - "sd1c295d2cf56": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/exports/node/index.js#L121-L160" - } - }, - "h3-pin": { - "name": "h3 request and Node adapter source at release commit", - "basis": "1.15.3", - "sources": { - "scfe5cba40964": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L275-L286", - "sdbe7e781611f": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L333-L344", - "s218970f40ae0": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/adapters/node.ts#L55-L89" - } - }, - "node-http": { - "name": "Node HTTP documentation", - "basis": "v22.22.1", - "sources": { - "sdc846082a7e3": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/http.md#L3653-L3657" - } - }, - "nuxt-lock": { - "name": "Nuxt lockfile", - "basis": "v4.0.0", - "sources": { - "se77b46c1e3ad": "https://github.com/nuxt/nuxt/blob/v4.0.0/pnpm-lock.yaml#L5204-L5205" - } - }, - "nuxt": { - "name": "Nuxt documentation", - "basis": "4.x", - "sources": { - "se2d1e4a2e433": "https://nuxt.com/docs/4.x/directory-structure/server", - "s32e447cb5ef4": "https://nuxt.com/docs/4.x/guide/going-further/runtime-config" - } - }, - "vite-pin": { - "name": "Vite bind source", - "basis": "v8.3.1", - "sources": { - "s36532f775f7f": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022", - "s8ba1c2b868b7": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/config.ts#L388-L396", - "s484948ac14b6": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/build.ts#L395" - } - }, - "vite": { - "name": "Vite documentation", - "basis": "unknown", - "sources": { - "sae9dd90908fc": "https://vite.dev/config/server-options", - "s16927b810cff": "https://vite.dev/guide/cli", - "s7c70a12e43ed": "https://vite.dev/config/build-options", - "sba6dbfdd7590": "https://vite.dev/guide/ssr" - } - }, - "grep": { - "name": "GNU grep", - "basis": "unknown", - "sources": { - "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", - "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", - "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" - } - }, - "nuxt-output": { - "name": "Nuxt output source", - "basis": "v4.0.0", - "sources": { - "sa65d64f55ca1": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L163-L170", - "se1cb3e5b4959": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L653-L659", - "sd00a53729513": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/package.json#L104" - } - }, - "nitro-output": { - "name": "Nitro output defaults", - "basis": "2.12.0", - "sources": { - "sbe9fd1f61fe0": "https://cdn.jsdelivr.net/npm/nitropack@2.12.0/dist/core/index.mjs" - } - }, - "nitro-node": { - "name": "nitropack 2.12.0 source", - "basis": "65e514d873da1d1d9edd4d9576d48d90b13e0fac", - "sources": { - "sd56c769d6dae": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/_resolve.ts#L85-L90", - "s32975278fc3f": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/preset.ts#L16-L29", - "sdfda9cd3ce7b": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/runtime/node-server.ts#L1-L23", - "sdf38c24e1f80": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/runtime/internal/app.ts#L59-L128", - "s91ab58ddc148": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/core/config/loader.ts#L134-L141", - "s0c8ca22542d2": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/package.json#L131" - } - }, - "nitro-output-docs": { - "name": "Nitro documentation (rolling)", - "basis": "unknown", - "sources": { - "s47fff9b24fba": "https://nitro.build/config", - "s51e38e411bfa": "https://nitro.build/docs/assets" - } - } - }, - "claims": { - "adapter-bind": {"text": "adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000.", "components": ["adapter", "svelte"], "sources": ["adapter:s011f4e44c91a", "adapter:s93f13e6b8d31", "adapter:sd23cac9a730f", "svelte:s59abf03bae6a"], "status": "REASONED"}, - "adapter-origin": {"text": "Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, - "adapter-ip": {"text": "ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, - "csrf-check": {"text": "csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, - "csrf-trust": {"text": "csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, - "svelte-public": {"text": "PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out.", "components": ["svelte"], "sources": ["svelte:sc183fdb68165", "svelte:s329dda89ba78"], "status": "REASONED"}, - "svelte-private": {"text": "Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds.", "components": ["svelte"], "sources": ["svelte:se2fdc4fd5885"], "status": "REASONED"}, - "svelte-routes": {"text": "Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each.", "components": ["svelte", "svelte-kit-pin"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sca07fbe19546", "svelte-kit-pin:sa9bbf285a91c", "svelte-kit-pin:s380cc33507e0", "svelte-kit-pin:sfb110948d2ca", "svelte-kit-pin:sffcf647d5328", "svelte-kit-pin:s0969ea98a4f1", "svelte-kit-pin:s6a78ad19d884"], "status": "REASONED"}, - "svelte-handle": {"text": "Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it.", "components": ["svelte", "svelte-kit-pin", "adapter"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sf5b873616d3c", "svelte-kit-pin:s5ad7a525e0ea", "svelte-kit-pin:s67fe32e7a18d", "svelte-kit-pin:sc4708064d4fd", "svelte-kit-pin:s95d5c7dd7a50", "svelte-kit-pin:sca363a35f3fc", "svelte-kit-pin:s89773d0c39e7", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b"], "status": "REASONED"}, - "nuxt-handlers": {"text": "Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, - "nuxt-auth": {"text": "Nuxt ships no built-in authentication; enforce access server-side, not with client redirects.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, - "nuxt-private": {"text": "Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, - "nuxt-public": {"text": "runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, - "vite-production": {"text": "Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform.", "components": ["vite"], "sources": ["vite:s16927b810cff"], "status": "REASONED"}, - "vite-bind": {"text": "Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests.", "components": ["vite-pin", "vite"], "sources": ["vite-pin:s36532f775f7f", "vite:sae9dd90908fc"], "status": "REASONED"}, - "vite-hosts": {"text": "server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names.", "components": ["vite"], "sources": ["vite:sae9dd90908fc"], "status": "REASONED"}, - "verify-private": {"text": "Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects.", "components": ["nuxt", "svelte"], "sources": ["nuxt:se2d1e4a2e433", "svelte:s329dda89ba78"], "status": "REASONED", "verify": [1]}, - "verify-secret": {"text": "Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root.", "components": ["grep", "adapter", "nuxt-output", "nitro-output", "vite-pin", "vite", "nitro-output-docs"], "sources": ["grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae", "adapter:s200d6e800c74", "adapter:sf17c5646623c", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b", "nuxt-output:sa65d64f55ca1", "nuxt-output:se1cb3e5b4959", "nitro-output:sbe9fd1f61fe0", "nitro-output-docs:s47fff9b24fba", "nitro-output-docs:s51e38e411bfa", "vite-pin:s8ba1c2b868b7", "vite-pin:s484948ac14b6", "vite:s7c70a12e43ed", "vite:sba6dbfdd7590"], "status": "REASONED", "verify": [1]}, - "verify-host": {"text": "The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers.", "components": ["vite", "adapter", "nitro-node", "svelte-kit-pin", "h3-pin", "node-http", "nuxt-lock"], "sources": ["vite:sae9dd90908fc", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:s0c8ca22542d2", "svelte-kit-pin:sd1c295d2cf56", "h3-pin:scfe5cba40964", "h3-pin:sdbe7e781611f", "h3-pin:s218970f40ae0", "node-http:sdc846082a7e3", "nuxt-lock:se77b46c1e3ad"], "status": "REASONED", "verify": [1]}, - "verify-proxy": {"text": "Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome.", "components": ["svelte", "nuxt", "adapter", "nuxt-output", "nitro-node"], "sources": ["svelte:s59abf03bae6a", "nuxt:s32e447cb5ef4", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nuxt-output:sd00a53729513", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:sdf38c24e1f80", "nitro-node:s91ab58ddc148", "nitro-node:s0c8ca22542d2"], "status": "REASONED", "verify": [1]} - } -} ---- # Full-stack JS frameworks: SvelteKit, Nuxt, Vite - -**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): - -- adapter: SvelteKit adapter-node (@sveltejs/adapter-node@5.5.7) -- svelte: SvelteKit documentation (unknown) -- svelte-kit-pin: SvelteKit routing and hooks documentation (@sveltejs/kit@2.70.3) -- h3-pin: h3 request and Node adapter source at release commit (1.15.3) -- node-http: Node HTTP documentation (v22.22.1) -- nuxt-lock: Nuxt lockfile (v4.0.0) -- nuxt: Nuxt documentation (4.x) -- vite-pin: Vite bind source (v8.3.1) -- vite: Vite documentation (unknown) -- grep: GNU grep (unknown) -- nuxt-output: Nuxt output source (v4.0.0) -- nitro-output: Nitro output defaults (2.12.0) -- nitro-node: nitropack 2.12.0 source (65e514d873da1d1d9edd4d9576d48d90b13e0fac) -- nitro-output-docs: Nitro documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): +Written against SvelteKit adapter-node @sveltejs/adapter-node@5.5.7, SvelteKit routing and hooks documentation @sveltejs/kit@2.70.3, Nuxt lockfile v4.0.0, Nuxt documentation 4.x, Nuxt output source v4.0.0, Vite bind source v8.3.1, and h3 request and Node adapter source at release commit 1.15.3, plus 2 more recorded bases; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- adapter-bind (REASONED; adapter, svelte): adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000. -- adapter-origin (REASONED; svelte): Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy. -- adapter-ip (REASONED; svelte): ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling. -- csrf-check (REASONED; svelte): csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks. -- csrf-trust (REASONED; svelte): csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged. -- svelte-public (REASONED; svelte): PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out. -- svelte-private (REASONED; svelte): Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds. -- svelte-routes (REASONED; svelte, svelte-kit-pin): Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each. -- svelte-handle (REASONED; svelte, svelte-kit-pin, adapter): Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it. -- nuxt-handlers (REASONED; nuxt): Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler. -- nuxt-auth (REASONED; nuxt): Nuxt ships no built-in authentication; enforce access server-side, not with client redirects. -- nuxt-private (REASONED; nuxt): Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them. -- nuxt-public (REASONED; nuxt): runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration. -- vite-production (REASONED; vite): Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform. -- vite-bind (REASONED; vite-pin, vite): Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests. -- vite-hosts (REASONED; vite): server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names. -- verify-private (REASONED; nuxt, svelte): Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects. -- verify-secret (REASONED; grep, adapter, nuxt-output, nitro-output, vite-pin, vite, nitro-output-docs): Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root. -- verify-host (REASONED; vite, adapter, nitro-node, svelte-kit-pin, h3-pin, node-http, nuxt-lock): The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers. -- verify-proxy (REASONED; svelte, nuxt, adapter, nuxt-output, nitro-node): Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome. - +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. SvelteKit, Nuxt, and Vite-based apps built by AI assistants inherit the same traps as [nextjs.md](nextjs.md): a server bind that can default wide open (SvelteKit's Node adapter binds `0.0.0.0` as of adapter-node 5.5.7, though Vite's dev server defaults to `localhost` as of Vite 8.3.1), a proxy that has to be explicitly trusted before secure cookies and correct origins work, and a public-env prefix that ships anything given it straight to the browser. Each framework also has more than one server entry point (endpoints, server routes, load functions), and a check placed in only one of them leaves the others open, exactly as with Next.js layouts versus Server Actions. @@ -384,3 +166,247 @@ REASONED: Host, proxy cookie, redirect and trusted-proxy checks per the cited Sv - Vite resolves an unset `server.host` to `'localhost'` (pinned tag v8.3.1): https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022 - SvelteKit adapter-node `host = env('HOST', '0.0.0.0')` and `port = env('PORT', !path && '3000')`, with `path = env('SOCKET_PATH', false)`, where `env()` returns a variable's value whenever it is present, even empty, and the socket-activation branch that listens on the passed descriptor instead (pinned tag @sveltejs/adapter-node@5.5.7): https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12, https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50 and https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60 - GNU grep `-f -` ("When file is '-', read patterns from standard input"), `-l` ("print the name of each input file from which output would normally have been printed") and its exit status (0 if a line is selected, 1 if none, 2 on error): https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html , https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html and https://www.gnu.org/software/grep/manual/html_node/Exit-Status.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/frontend-frameworks.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): + +- adapter: SvelteKit adapter-node (@sveltejs/adapter-node@5.5.7) +- svelte-kit-pin: SvelteKit routing and hooks documentation (@sveltejs/kit@2.70.3) +- nuxt-lock: Nuxt lockfile (v4.0.0) +- nuxt: Nuxt documentation (4.x) +- nuxt-output: Nuxt output source (v4.0.0) +- vite-pin: Vite bind source (v8.3.1) +- svelte: SvelteKit documentation (unknown) +- vite: Vite documentation (unknown) +- h3-pin: h3 request and Node adapter source at release commit (1.15.3) +- node-http: Node HTTP documentation (v22.22.1) +- nitro-output: Nitro output defaults (2.12.0) +- nitro-node: nitropack 2.12.0 source (65e514d873da1d1d9edd4d9576d48d90b13e0fac) +- nitro-output-docs: Nitro documentation (rolling) (unknown) +- grep: GNU grep (unknown) + +Claims (full source mapping in the JSON block below): + +- adapter-bind (REASONED; adapter, svelte): adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000. +- adapter-origin (REASONED; svelte): Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy. +- adapter-ip (REASONED; svelte): ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling. +- csrf-check (REASONED; svelte): csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks. +- csrf-trust (REASONED; svelte): csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged. +- svelte-public (REASONED; svelte): PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out. +- svelte-private (REASONED; svelte): Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds. +- svelte-routes (REASONED; svelte, svelte-kit-pin): Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each. +- svelte-handle (REASONED; svelte, svelte-kit-pin, adapter): Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it. +- nuxt-handlers (REASONED; nuxt): Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler. +- nuxt-auth (REASONED; nuxt): Nuxt ships no built-in authentication; enforce access server-side, not with client redirects. +- nuxt-private (REASONED; nuxt): Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them. +- nuxt-public (REASONED; nuxt): runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration. +- vite-production (REASONED; vite): Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform. +- vite-bind (REASONED; vite-pin, vite): Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests. +- vite-hosts (REASONED; vite): server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names. +- verify-private (REASONED; nuxt, svelte): Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects. +- verify-secret (REASONED; grep, adapter, nuxt-output, nitro-output, vite-pin, vite, nitro-output-docs): Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root. +- verify-host (REASONED; vite, adapter, nitro-node, svelte-kit-pin, h3-pin, node-http, nuxt-lock): The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers. +- verify-proxy (REASONED; svelte, nuxt, adapter, nuxt-output, nitro-node): Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "c967988728d40cccfa0b3e543320cd6e6d4f7568099a9805fb91a941243d4c1d", + "components": { + "adapter": { + "name": "SvelteKit adapter-node", + "basis": "@sveltejs/adapter-node@5.5.7", + "sources": { + "s011f4e44c91a": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50", + "s93f13e6b8d31": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12", + "sd23cac9a730f": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60", + "s200d6e800c74": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L24-L38", + "sf17c5646623c": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L118", + "s1ffeda9bbb37": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L69", + "s8d4fa8e7426b": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L243", + "sfd51397a510e": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L17-L24", + "s6152d66c6b19": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L95-L105", + "s7b3a11637166": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L209-L239" + } + }, + "svelte-kit-pin": { + "name": "SvelteKit routing and hooks documentation", + "basis": "@sveltejs/kit@2.70.3", + "sources": { + "sca07fbe19546": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/10-routing.md#L291-L332", + "sa9bbf285a91c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/20-load.md#L702-L714", + "s380cc33507e0": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/30-form-actions.md#L1-L5", + "sf5b873616d3c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/30-advanced/20-hooks.md#L15-L36", + "s5ad7a525e0ea": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/60-remote-functions.md#L1300-L1303", + "s6a78ad19d884": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L70-L78", + "s0969ea98a4f1": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L178-L250", + "sffcf647d5328": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/data/index.js#L39-L97", + "sfb110948d2ca": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/client/client.js#L1217-L1235", + "s67fe32e7a18d": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L73-L106", + "sc4708064d4fd": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L111-L130", + "s95d5c7dd7a50": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L333-L343", + "sca363a35f3fc": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L456-L498", + "s89773d0c39e7": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L274-L306", + "sd1c295d2cf56": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/exports/node/index.js#L121-L160" + } + }, + "nuxt-lock": { + "name": "Nuxt lockfile", + "basis": "v4.0.0", + "sources": { + "se77b46c1e3ad": "https://github.com/nuxt/nuxt/blob/v4.0.0/pnpm-lock.yaml#L5204-L5205" + } + }, + "nuxt": { + "name": "Nuxt documentation", + "basis": "4.x", + "sources": { + "se2d1e4a2e433": "https://nuxt.com/docs/4.x/directory-structure/server", + "s32e447cb5ef4": "https://nuxt.com/docs/4.x/guide/going-further/runtime-config" + } + }, + "nuxt-output": { + "name": "Nuxt output source", + "basis": "v4.0.0", + "sources": { + "sa65d64f55ca1": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L163-L170", + "se1cb3e5b4959": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L653-L659", + "sd00a53729513": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/package.json#L104" + } + }, + "vite-pin": { + "name": "Vite bind source", + "basis": "v8.3.1", + "sources": { + "s36532f775f7f": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022", + "s8ba1c2b868b7": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/config.ts#L388-L396", + "s484948ac14b6": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/build.ts#L395" + } + }, + "svelte": { + "name": "SvelteKit documentation", + "basis": "unknown", + "sources": { + "sc183fdb68165": "https://svelte.dev/docs/kit/$env-static-public", + "s59abf03bae6a": "https://svelte.dev/docs/kit/adapter-node", + "s329dda89ba78": "https://svelte.dev/docs/kit/configuration", + "se2fdc4fd5885": "https://svelte.dev/docs/kit/server-only-modules" + } + }, + "vite": { + "name": "Vite documentation", + "basis": "unknown", + "sources": { + "sae9dd90908fc": "https://vite.dev/config/server-options", + "s16927b810cff": "https://vite.dev/guide/cli", + "s7c70a12e43ed": "https://vite.dev/config/build-options", + "sba6dbfdd7590": "https://vite.dev/guide/ssr" + } + }, + "h3-pin": { + "name": "h3 request and Node adapter source at release commit", + "basis": "1.15.3", + "sources": { + "scfe5cba40964": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L275-L286", + "sdbe7e781611f": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L333-L344", + "s218970f40ae0": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/adapters/node.ts#L55-L89" + } + }, + "node-http": { + "name": "Node HTTP documentation", + "basis": "v22.22.1", + "sources": { + "sdc846082a7e3": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/http.md#L3653-L3657" + } + }, + "nitro-output": { + "name": "Nitro output defaults", + "basis": "2.12.0", + "sources": { + "sbe9fd1f61fe0": "https://cdn.jsdelivr.net/npm/nitropack@2.12.0/dist/core/index.mjs" + } + }, + "nitro-node": { + "name": "nitropack 2.12.0 source", + "basis": "65e514d873da1d1d9edd4d9576d48d90b13e0fac", + "sources": { + "sd56c769d6dae": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/_resolve.ts#L85-L90", + "s32975278fc3f": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/preset.ts#L16-L29", + "sdfda9cd3ce7b": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/runtime/node-server.ts#L1-L23", + "sdf38c24e1f80": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/runtime/internal/app.ts#L59-L128", + "s91ab58ddc148": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/core/config/loader.ts#L134-L141", + "s0c8ca22542d2": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/package.json#L131" + } + }, + "nitro-output-docs": { + "name": "Nitro documentation (rolling)", + "basis": "unknown", + "sources": { + "s47fff9b24fba": "https://nitro.build/config", + "s51e38e411bfa": "https://nitro.build/docs/assets" + } + }, + "grep": { + "name": "GNU grep", + "basis": "unknown", + "sources": { + "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", + "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", + "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" + } + } + }, + "claims": { + "adapter-bind": {"text": "adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000.", "components": ["adapter", "svelte"], "sources": ["adapter:s011f4e44c91a", "adapter:s93f13e6b8d31", "adapter:sd23cac9a730f", "svelte:s59abf03bae6a"], "status": "REASONED"}, + "adapter-origin": {"text": "Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, + "adapter-ip": {"text": "ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, + "csrf-check": {"text": "csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, + "csrf-trust": {"text": "csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, + "svelte-public": {"text": "PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out.", "components": ["svelte"], "sources": ["svelte:sc183fdb68165", "svelte:s329dda89ba78"], "status": "REASONED"}, + "svelte-private": {"text": "Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds.", "components": ["svelte"], "sources": ["svelte:se2fdc4fd5885"], "status": "REASONED"}, + "svelte-routes": {"text": "Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each.", "components": ["svelte", "svelte-kit-pin"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sca07fbe19546", "svelte-kit-pin:sa9bbf285a91c", "svelte-kit-pin:s380cc33507e0", "svelte-kit-pin:sfb110948d2ca", "svelte-kit-pin:sffcf647d5328", "svelte-kit-pin:s0969ea98a4f1", "svelte-kit-pin:s6a78ad19d884"], "status": "REASONED"}, + "svelte-handle": {"text": "Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it.", "components": ["svelte", "svelte-kit-pin", "adapter"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sf5b873616d3c", "svelte-kit-pin:s5ad7a525e0ea", "svelte-kit-pin:s67fe32e7a18d", "svelte-kit-pin:sc4708064d4fd", "svelte-kit-pin:s95d5c7dd7a50", "svelte-kit-pin:sca363a35f3fc", "svelte-kit-pin:s89773d0c39e7", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b"], "status": "REASONED"}, + "nuxt-handlers": {"text": "Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, + "nuxt-auth": {"text": "Nuxt ships no built-in authentication; enforce access server-side, not with client redirects.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, + "nuxt-private": {"text": "Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, + "nuxt-public": {"text": "runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, + "vite-production": {"text": "Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform.", "components": ["vite"], "sources": ["vite:s16927b810cff"], "status": "REASONED"}, + "vite-bind": {"text": "Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests.", "components": ["vite-pin", "vite"], "sources": ["vite-pin:s36532f775f7f", "vite:sae9dd90908fc"], "status": "REASONED"}, + "vite-hosts": {"text": "server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names.", "components": ["vite"], "sources": ["vite:sae9dd90908fc"], "status": "REASONED"}, + "verify-private": {"text": "Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects.", "components": ["nuxt", "svelte"], "sources": ["nuxt:se2d1e4a2e433", "svelte:s329dda89ba78"], "status": "REASONED", "verify": [1]}, + "verify-secret": {"text": "Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root.", "components": ["grep", "adapter", "nuxt-output", "nitro-output", "vite-pin", "vite", "nitro-output-docs"], "sources": ["grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae", "adapter:s200d6e800c74", "adapter:sf17c5646623c", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b", "nuxt-output:sa65d64f55ca1", "nuxt-output:se1cb3e5b4959", "nitro-output:sbe9fd1f61fe0", "nitro-output-docs:s47fff9b24fba", "nitro-output-docs:s51e38e411bfa", "vite-pin:s8ba1c2b868b7", "vite-pin:s484948ac14b6", "vite:s7c70a12e43ed", "vite:sba6dbfdd7590"], "status": "REASONED", "verify": [1]}, + "verify-host": {"text": "The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers.", "components": ["vite", "adapter", "nitro-node", "svelte-kit-pin", "h3-pin", "node-http", "nuxt-lock"], "sources": ["vite:sae9dd90908fc", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:s0c8ca22542d2", "svelte-kit-pin:sd1c295d2cf56", "h3-pin:scfe5cba40964", "h3-pin:sdbe7e781611f", "h3-pin:s218970f40ae0", "node-http:sdc846082a7e3", "nuxt-lock:se77b46c1e3ad"], "status": "REASONED", "verify": [1]}, + "verify-proxy": {"text": "Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome.", "components": ["svelte", "nuxt", "adapter", "nuxt-output", "nitro-node"], "sources": ["svelte:s59abf03bae6a", "nuxt:s32e447cb5ef4", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nuxt-output:sd00a53729513", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:sdf38c24e1f80", "nitro-node:s91ab58ddc148", "nitro-node:s0c8ca22542d2"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/gpu-clouds.md b/gpu-clouds.md index 57411011e..5c857761a 100644 --- a/gpu-clouds.md +++ b/gpu-clouds.md @@ -1,225 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "7dc59d278092451cbf9bc33836b92a411a629f5285413f75b1409ecbf11a9774", - "components": { - "runpod": { - "name": "RunPod documentation", - "basis": "unknown", - "sources": { - "s817198f039ee": "https://docs.runpod.io/pods/configuration/expose-ports", - "s13c43b856d4f": "https://docs.runpod.io/pods/configuration/use-ssh" - } - }, - "vast": { - "name": "Vast.ai documentation", - "basis": "unknown", - "sources": { - "sc25989a1613e": "https://docs.vast.ai/guides/instances/connect/networking", - "s0bdbda534723": "https://docs.vast.ai/guides/instances/connect/instance-portal", - "s6e0303ef19fe": "https://docs.vast.ai/guides/instances/connect/ssh" - } - }, - "vast-image": { - "name": "Vast.ai base image", - "basis": "00064421641881c1f83ff58c55f13cbc10cbea4d", - "sources": { - "sf75c3c07819e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md", - "s5d2b91002bac": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L175-L187", - "s229c9334547e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L135-L147", - "s3b735c4b3632": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L154-L158", - "s037a7cbd778c": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/instance-tools/tests/base/26-caddy-auth.sh#L97-L110", - "sdc669faa429d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/caddy.sh#L9-L14", - "s0428d55e9145": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24", - "s1b8c6c46f358": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50", - "s44f3439f13de": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80", - "sd0ba21945c9a": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L14-L40", - "sc77a7717bbc4": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L68-L71", - "s3c43b58f9e41": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L75-L93", - "sf1b910a375c6": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L125-L132", - "sae1085635ff9": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L175-L186", - "sd9c32e411f08": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L212-L240", - "s5a43e0cf8348": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L293-L308", - "sea35d951abe5": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L311-L321", - "s4a05112af859": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L323-L387", - "s5c18e60bb9e7": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L400-L413", - "s92be3bd5cdef": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/launch.sh#L77-L92", - "s7ac7955e604d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/syncthing.sh#L14-L16", - "s10acc068e127": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/etc/vast_boot.d/10-prep-env.sh#L12-L27", - "s6d0bc9f2a0da": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/capabilities/manifest.py#L674-L694" - } - }, - "lambda": { - "name": "Lambda Public Cloud documentation", - "basis": "unknown", - "sources": { - "se22064748974": "https://docs.lambda.ai/public-cloud/firewalls/", - "sb12b4a953833": "https://docs.lambda.ai/public-cloud/on-demand/connecting-instance/" - } - }, - "modal": { - "name": "Modal proxy authentication", - "basis": "unknown", - "sources": { - "s7d083f819a21": "https://modal.com/docs/guide/webhook-proxy-auth", - "s12a4f5e04dbe": "https://modal.com/docs/guide/shared-endpoints", - "s33e1c2c2faf3": "https://modal.com/docs/guide/endpoints" - } - }, - "jupyter": { - "name": "Jupyter Server security", - "basis": "unknown", - "sources": { - "sc703a8030c8f": "https://jupyter-server.readthedocs.io/en/latest/operators/security.html" - } - }, - "jupyter-contents": { - "name": "Jupyter Server contents authentication", - "basis": "v2.18.0", - "sources": { - "sdc3ad9152505": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L83-L186", - "sf0a3ea8ee42e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L436-L445", - "s4bdc7b9183f3": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L782-L789", - "s4e7504ed1ac8": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L1201", - "s52316685e1e4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L465-L516", - "s83f1081e863e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L255-L298", - "s6c884bdf8fb4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L6-L26", - "sb7fab0e85c94": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L315-L322", - "s58a8a77857b5": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/api/api.yaml#L82-L137" - } - }, - "iproute2": { - "name": "iproute2 ss manual", - "basis": "v6.12.0", - "sources": { - "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", - "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", - "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", - "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375", - "s6c87acff18cf": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L358-L359", - "sf8fdfa16eb3f": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L6114-L6117", - "s1e96518d2ede": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L4235-L4240" - } - }, - "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" - } - }, - "ollama": { - "name": "Ollama", - "basis": "v0.34.2", - "sources": { - "s96f77aee8bdd": "https://github.com/ollama/ollama/blob/v0.34.2/server/routes.go", - "sd146dc5d917d": "https://github.com/ollama/ollama/blob/v0.34.2/api/client.go", - "s7478dad099ab": "https://github.com/ollama/ollama/blob/v0.34.2/cmd/cmd.go", - "s2abe15b73bce": "https://github.com/ollama/ollama/blob/v0.34.2/envconfig/config.go" - } - } - }, - "claims": { - "publication": {"text": "Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover.", "components": ["runpod", "vast", "lambda", "modal"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "lambda:se22064748974", "modal:s7d083f819a21"], "status": "REASONED"}, - "runpod-http": {"text": "Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "runpod-tcp": {"text": "Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "runpod-symmetry": {"text": "TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "vast-ssh-port": {"text": "Vast.ai SSH launch mode opens internal port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, - "vast-jupyter-port": {"text": "Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, - "vast-mapping": {"text": "Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, - "vast-proxy": {"text": "Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, - "vast-token": {"text": "Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, - "vast-auth-controls": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results.", "components": ["vast-image", "vast"], "sources": ["vast-image:sf75c3c07819e", "vast:s0bdbda534723", "vast-image:s5d2b91002bac", "vast-image:s229c9334547e", "vast-image:s3b735c4b3632", "vast-image:s037a7cbd778c", "vast-image:sdc669faa429d", "vast-image:sc77a7717bbc4", "vast-image:s3c43b58f9e41", "vast-image:sf1b910a375c6", "vast-image:sd9c32e411f08", "vast-image:sea35d951abe5", "vast-image:s4a05112af859"], "status": "REASONED"}, - "vast-portal-routing": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential.", "components": ["vast-image"], "sources": ["vast-image:sd0ba21945c9a", "vast-image:sd9c32e411f08"], "status": "REASONED"}, - "vast-portal-jupyter": {"text": "Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication.", "components": ["vast-image"], "sources": ["vast-image:s10acc068e127", "vast-image:sd9c32e411f08", "vast-image:sd0ba21945c9a"], "status": "REASONED"}, - "vast-auth-defaults": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set.", "components": ["vast-image"], "sources": ["vast-image:s3c43b58f9e41"], "status": "REASONED"}, - "vast-auth-routes": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD.", "components": ["vast-image"], "sources": ["vast-image:sf1b910a375c6", "vast-image:s4a05112af859", "vast-image:sea35d951abe5"], "status": "REASONED"}, - "vast-auth-exemptions": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there.", "components": ["vast-image"], "sources": ["vast-image:sc77a7717bbc4", "vast-image:sd9c32e411f08", "vast-image:s5a43e0cf8348", "vast-image:sae1085635ff9", "vast-image:s4a05112af859"], "status": "REASONED"}, - "vast-token-reuse": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too.", "components": ["vast-image"], "sources": ["vast-image:s5c18e60bb9e7", "vast-image:s92be3bd5cdef", "vast-image:s7ac7955e604d"], "status": "REASONED"}, - "vast-manifest-report": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to \"true\": reporting tests equality to \"true\", enforcement inequality to \"false\". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce.", "components": ["vast-image"], "sources": ["vast-image:s6d0bc9f2a0da", "vast-image:s3c43b58f9e41"], "status": "REASONED"}, - "lambda-default": {"text": "Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, - "lambda-rule": {"text": "An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, - "modal-web": {"text": "Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "modal-dedicated": {"text": "Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "modal-server": {"text": "Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "modal-shared": {"text": "Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public.", "components": ["modal"], "sources": ["modal:s7d083f819a21", "modal:s12a4f5e04dbe", "modal:s33e1c2c2faf3"], "status": "REASONED"}, - "modal-credentials": {"text": "Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer .; missing credentials on a protected endpoint return 401.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "runpod-bind": {"text": "RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "listener-auth": {"text": "Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication.", "components": ["runpod", "vast", "jupyter", "ollama"], "sources": ["runpod:s817198f039ee", "vast:s0bdbda534723", "jupyter:sc703a8030c8f", "ollama:s96f77aee8bdd", "ollama:sd146dc5d917d", "ollama:s7478dad099ab", "ollama:s2abe15b73bce"], "status": "REASONED"}, - "mfa": {"text": "MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login.", "components": ["modal", "jupyter"], "sources": ["modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED"}, - "lambda-ssh": {"text": "Lambda requires an SSH key at launch; keep the private key off the instance.", "components": ["lambda"], "sources": ["lambda:sb12b4a953833"], "status": "REASONED"}, - "vast-ssh": {"text": "Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance.", "components": ["vast"], "sources": ["vast:s6e0303ef19fe"], "status": "REASONED"}, - "runpod-ssh": {"text": "RunPod recommends SSH keys and offers an optional password; use the key and skip the password.", "components": ["runpod"], "sources": ["runpod:s13c43b856d4f"], "status": "REASONED"}, - "verify-inventory": {"text": "This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside.", "components": ["runpod", "vast", "iproute2", "network-namespaces"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850", "iproute2:s6c87acff18cf", "iproute2:sf8fdfa16eb3f", "iproute2:s1e96518d2ede", "network-namespaces:s427c923e346f"], "status": "REASONED", "verify": [1]}, - "verify-jupyter": {"text": "This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls.", "components": ["jupyter", "jupyter-contents"], "sources": ["jupyter:sc703a8030c8f", "jupyter-contents:sdc3ad9152505", "jupyter-contents:sf0a3ea8ee42e", "jupyter-contents:s4bdc7b9183f3", "jupyter-contents:s4e7504ed1ac8", "jupyter-contents:s52316685e1e4", "jupyter-contents:s83f1081e863e", "jupyter-contents:s6c884bdf8fb4", "jupyter-contents:sb7fab0e85c94", "jupyter-contents:s58a8a77857b5"], "status": "REASONED", "verify": [1]}, - "verify-mapping": {"text": "Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control.", "components": ["runpod", "vast", "modal", "jupyter"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED", "verify": [1]}, - "verify-portal-exempt": {"text": "For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE.", "components": ["vast-image"], "sources": ["vast-image:sae1085635ff9", "vast-image:s4a05112af859", "vast-image:sd9c32e411f08", "vast-image:s3c43b58f9e41"], "status": "REASONED", "verify": [2]}, - "remote-desktop": {"text": "Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it.", "components": ["runpod", "vast", "vast-image"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast-image:s0428d55e9145", "vast-image:s1b8c6c46f358", "vast-image:s44f3439f13de"], "status": "REASONED"} - } -} ---- # Rented GPUs: RunPod, Vast.ai, Lambda, Modal - -**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. +Written against RunPod documentation (no recorded version), Jupyter Server contents authentication v2.18.0, Ollama v0.34.2, iproute2 ss manual v6.12.0, and Linux network namespaces manual man-pages-5.13; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 35 claims, all reasoned from cited sources. -Components (recorded basis): - -- runpod: RunPod documentation (unknown) -- vast: Vast.ai documentation (unknown) -- vast-image: Vast.ai base image (00064421641881c1f83ff58c55f13cbc10cbea4d) -- lambda: Lambda Public Cloud documentation (unknown) -- modal: Modal proxy authentication (unknown) -- jupyter: Jupyter Server security (unknown) -- jupyter-contents: Jupyter Server contents authentication (v2.18.0) -- iproute2: iproute2 ss manual (v6.12.0) -- network-namespaces: Linux network namespaces manual (man-pages-5.13) -- ollama: Ollama (v0.34.2) - -Claims (full source mapping in the front matter): - -- publication (REASONED; runpod, vast, lambda, modal): Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover. -- runpod-http (REASONED; runpod): Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication. -- runpod-tcp (REASONED; runpod): Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data. -- runpod-symmetry (REASONED; runpod): TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public. -- vast-ssh-port (REASONED; vast): Vast.ai SSH launch mode opens internal port 22 by default. -- vast-jupyter-port (REASONED; vast): Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default. -- vast-mapping (REASONED; vast): Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step. -- vast-proxy (REASONED; vast): Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation. -- vast-token (REASONED; vast): Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates. -- vast-auth-controls (REASONED; vast-image, vast): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results. -- vast-portal-routing (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential. -- vast-portal-jupyter (REASONED; vast-image): Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication. -- vast-auth-defaults (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set. -- vast-auth-routes (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD. -- vast-auth-exemptions (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there. -- vast-token-reuse (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too. -- vast-manifest-report (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to "true": reporting tests equality to "true", enforcement inequality to "false". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce. -- lambda-default (REASONED; lambda): Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch. -- lambda-rule (REASONED; lambda): An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access. -- modal-web (REASONED; modal): Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication. -- modal-dedicated (REASONED; modal): Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public. -- modal-server (REASONED; modal): Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public. -- modal-shared (REASONED; modal): Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public. -- modal-credentials (REASONED; modal): Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer <token_id>.<token_secret>; missing credentials on a protected endpoint return 401. -- runpod-bind (REASONED; runpod): RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost. -- listener-auth (REASONED; runpod, vast, jupyter, ollama): Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication. -- mfa (REASONED; modal, jupyter): MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login. -- lambda-ssh (REASONED; lambda): Lambda requires an SSH key at launch; keep the private key off the instance. -- vast-ssh (REASONED; vast): Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance. -- runpod-ssh (REASONED; runpod): RunPod recommends SSH keys and offers an optional password; use the key and skip the password. -- verify-inventory (REASONED; runpod, vast, iproute2, network-namespaces): This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside. -- verify-jupyter (REASONED; jupyter, jupyter-contents): This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls. -- verify-mapping (REASONED; runpod, vast, modal, jupyter): Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control. -- verify-portal-exempt (REASONED; vast-image): For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE. -- remote-desktop (REASONED; runpod, vast, vast-image): Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it. - +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. Unlike the major clouds, most rented-GPU platforms put no default-deny firewall in front of the box. How a bound port becomes public differs by platform: on RunPod you expose a port (HTTP or TCP), on Vast.ai a launch mode or an added port maps it to a public port, on Lambda you open a firewall rule (its inbound firewall is deny-by-default), and on Modal it depends on the serverless construct. Raw port publication and a firewall allowance add NO authentication of their own, so a listener exposed that way is reachable with only whatever auth it has itself; an authenticating platform proxy is the exception (Modal's Endpoints and Servers require proxy auth by default and Shared Endpoints always do, only a public Web endpoint does not, and Vast.ai's Instance Portal gates the routes it proxies), but do not assume one is in front of a given port. Treat every listener the same way you would on your own hardware: bind it privately or authenticate it, per [ollama.md](ollama.md) and [model-servers.md](model-servers.md). @@ -424,3 +207,246 @@ REASONED: following block; this guide's test design for the Vast.ai portal's pat - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop x11vnc port and password fallback; does not establish password strength (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24 - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop browser and direct connection choices (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50 - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop documented VNC password and override (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80 + + + +## 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/gpu-clouds.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): + +- runpod: RunPod documentation (unknown) +- vast: Vast.ai documentation (unknown) +- vast-image: Vast.ai base image (00064421641881c1f83ff58c55f13cbc10cbea4d) +- lambda: Lambda Public Cloud documentation (unknown) +- modal: Modal proxy authentication (unknown) +- jupyter: Jupyter Server security (unknown) +- jupyter-contents: Jupyter Server contents authentication (v2.18.0) +- ollama: Ollama (v0.34.2) +- iproute2: iproute2 ss manual (v6.12.0) +- network-namespaces: Linux network namespaces manual (man-pages-5.13) + +Claims (full source mapping in the JSON block below): + +- publication (REASONED; runpod, vast, lambda, modal): Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover. +- runpod-http (REASONED; runpod): Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication. +- runpod-tcp (REASONED; runpod): Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data. +- runpod-symmetry (REASONED; runpod): TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public. +- vast-ssh-port (REASONED; vast): Vast.ai SSH launch mode opens internal port 22 by default. +- vast-jupyter-port (REASONED; vast): Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default. +- vast-mapping (REASONED; vast): Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step. +- vast-proxy (REASONED; vast): Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation. +- vast-token (REASONED; vast): Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates. +- vast-auth-controls (REASONED; vast-image, vast): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results. +- vast-portal-routing (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential. +- vast-portal-jupyter (REASONED; vast-image): Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication. +- vast-auth-defaults (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set. +- vast-auth-routes (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD. +- vast-auth-exemptions (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there. +- vast-token-reuse (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too. +- vast-manifest-report (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to "true": reporting tests equality to "true", enforcement inequality to "false". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce. +- lambda-default (REASONED; lambda): Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch. +- lambda-rule (REASONED; lambda): An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access. +- modal-web (REASONED; modal): Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication. +- modal-dedicated (REASONED; modal): Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public. +- modal-server (REASONED; modal): Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public. +- modal-shared (REASONED; modal): Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public. +- modal-credentials (REASONED; modal): Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer <token_id>.<token_secret>; missing credentials on a protected endpoint return 401. +- runpod-bind (REASONED; runpod): RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost. +- listener-auth (REASONED; runpod, vast, jupyter, ollama): Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication. +- mfa (REASONED; modal, jupyter): MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login. +- lambda-ssh (REASONED; lambda): Lambda requires an SSH key at launch; keep the private key off the instance. +- vast-ssh (REASONED; vast): Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance. +- runpod-ssh (REASONED; runpod): RunPod recommends SSH keys and offers an optional password; use the key and skip the password. +- verify-inventory (REASONED; runpod, vast, iproute2, network-namespaces): This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside. +- verify-jupyter (REASONED; jupyter, jupyter-contents): This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls. +- verify-mapping (REASONED; runpod, vast, modal, jupyter): Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control. +- verify-portal-exempt (REASONED; vast-image): For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE. +- remote-desktop (REASONED; runpod, vast, vast-image): Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "7dc59d278092451cbf9bc33836b92a411a629f5285413f75b1409ecbf11a9774", + "components": { + "runpod": { + "name": "RunPod documentation", + "basis": "unknown", + "sources": { + "s817198f039ee": "https://docs.runpod.io/pods/configuration/expose-ports", + "s13c43b856d4f": "https://docs.runpod.io/pods/configuration/use-ssh" + } + }, + "vast": { + "name": "Vast.ai documentation", + "basis": "unknown", + "sources": { + "sc25989a1613e": "https://docs.vast.ai/guides/instances/connect/networking", + "s0bdbda534723": "https://docs.vast.ai/guides/instances/connect/instance-portal", + "s6e0303ef19fe": "https://docs.vast.ai/guides/instances/connect/ssh" + } + }, + "vast-image": { + "name": "Vast.ai base image", + "basis": "00064421641881c1f83ff58c55f13cbc10cbea4d", + "sources": { + "sf75c3c07819e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md", + "s5d2b91002bac": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L175-L187", + "s229c9334547e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L135-L147", + "s3b735c4b3632": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L154-L158", + "s037a7cbd778c": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/instance-tools/tests/base/26-caddy-auth.sh#L97-L110", + "sdc669faa429d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/caddy.sh#L9-L14", + "s0428d55e9145": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24", + "s1b8c6c46f358": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50", + "s44f3439f13de": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80", + "sd0ba21945c9a": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L14-L40", + "sc77a7717bbc4": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L68-L71", + "s3c43b58f9e41": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L75-L93", + "sf1b910a375c6": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L125-L132", + "sae1085635ff9": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L175-L186", + "sd9c32e411f08": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L212-L240", + "s5a43e0cf8348": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L293-L308", + "sea35d951abe5": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L311-L321", + "s4a05112af859": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L323-L387", + "s5c18e60bb9e7": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L400-L413", + "s92be3bd5cdef": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/launch.sh#L77-L92", + "s7ac7955e604d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/syncthing.sh#L14-L16", + "s10acc068e127": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/etc/vast_boot.d/10-prep-env.sh#L12-L27", + "s6d0bc9f2a0da": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/capabilities/manifest.py#L674-L694" + } + }, + "lambda": { + "name": "Lambda Public Cloud documentation", + "basis": "unknown", + "sources": { + "se22064748974": "https://docs.lambda.ai/public-cloud/firewalls/", + "sb12b4a953833": "https://docs.lambda.ai/public-cloud/on-demand/connecting-instance/" + } + }, + "modal": { + "name": "Modal proxy authentication", + "basis": "unknown", + "sources": { + "s7d083f819a21": "https://modal.com/docs/guide/webhook-proxy-auth", + "s12a4f5e04dbe": "https://modal.com/docs/guide/shared-endpoints", + "s33e1c2c2faf3": "https://modal.com/docs/guide/endpoints" + } + }, + "jupyter": { + "name": "Jupyter Server security", + "basis": "unknown", + "sources": { + "sc703a8030c8f": "https://jupyter-server.readthedocs.io/en/latest/operators/security.html" + } + }, + "jupyter-contents": { + "name": "Jupyter Server contents authentication", + "basis": "v2.18.0", + "sources": { + "sdc3ad9152505": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L83-L186", + "sf0a3ea8ee42e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L436-L445", + "s4bdc7b9183f3": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L782-L789", + "s4e7504ed1ac8": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L1201", + "s52316685e1e4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L465-L516", + "s83f1081e863e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L255-L298", + "s6c884bdf8fb4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L6-L26", + "sb7fab0e85c94": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L315-L322", + "s58a8a77857b5": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/api/api.yaml#L82-L137" + } + }, + "ollama": { + "name": "Ollama", + "basis": "v0.34.2", + "sources": { + "s96f77aee8bdd": "https://github.com/ollama/ollama/blob/v0.34.2/server/routes.go", + "sd146dc5d917d": "https://github.com/ollama/ollama/blob/v0.34.2/api/client.go", + "s7478dad099ab": "https://github.com/ollama/ollama/blob/v0.34.2/cmd/cmd.go", + "s2abe15b73bce": "https://github.com/ollama/ollama/blob/v0.34.2/envconfig/config.go" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.12.0", + "sources": { + "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", + "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", + "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", + "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375", + "s6c87acff18cf": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L358-L359", + "sf8fdfa16eb3f": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L6114-L6117", + "s1e96518d2ede": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L4235-L4240" + } + }, + "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": { + "publication": {"text": "Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover.", "components": ["runpod", "vast", "lambda", "modal"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "lambda:se22064748974", "modal:s7d083f819a21"], "status": "REASONED"}, + "runpod-http": {"text": "Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "runpod-tcp": {"text": "Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "runpod-symmetry": {"text": "TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "vast-ssh-port": {"text": "Vast.ai SSH launch mode opens internal port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-jupyter-port": {"text": "Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-mapping": {"text": "Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-proxy": {"text": "Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, + "vast-token": {"text": "Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, + "vast-auth-controls": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results.", "components": ["vast-image", "vast"], "sources": ["vast-image:sf75c3c07819e", "vast:s0bdbda534723", "vast-image:s5d2b91002bac", "vast-image:s229c9334547e", "vast-image:s3b735c4b3632", "vast-image:s037a7cbd778c", "vast-image:sdc669faa429d", "vast-image:sc77a7717bbc4", "vast-image:s3c43b58f9e41", "vast-image:sf1b910a375c6", "vast-image:sd9c32e411f08", "vast-image:sea35d951abe5", "vast-image:s4a05112af859"], "status": "REASONED"}, + "vast-portal-routing": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential.", "components": ["vast-image"], "sources": ["vast-image:sd0ba21945c9a", "vast-image:sd9c32e411f08"], "status": "REASONED"}, + "vast-portal-jupyter": {"text": "Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication.", "components": ["vast-image"], "sources": ["vast-image:s10acc068e127", "vast-image:sd9c32e411f08", "vast-image:sd0ba21945c9a"], "status": "REASONED"}, + "vast-auth-defaults": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set.", "components": ["vast-image"], "sources": ["vast-image:s3c43b58f9e41"], "status": "REASONED"}, + "vast-auth-routes": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD.", "components": ["vast-image"], "sources": ["vast-image:sf1b910a375c6", "vast-image:s4a05112af859", "vast-image:sea35d951abe5"], "status": "REASONED"}, + "vast-auth-exemptions": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there.", "components": ["vast-image"], "sources": ["vast-image:sc77a7717bbc4", "vast-image:sd9c32e411f08", "vast-image:s5a43e0cf8348", "vast-image:sae1085635ff9", "vast-image:s4a05112af859"], "status": "REASONED"}, + "vast-token-reuse": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too.", "components": ["vast-image"], "sources": ["vast-image:s5c18e60bb9e7", "vast-image:s92be3bd5cdef", "vast-image:s7ac7955e604d"], "status": "REASONED"}, + "vast-manifest-report": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to \"true\": reporting tests equality to \"true\", enforcement inequality to \"false\". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce.", "components": ["vast-image"], "sources": ["vast-image:s6d0bc9f2a0da", "vast-image:s3c43b58f9e41"], "status": "REASONED"}, + "lambda-default": {"text": "Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, + "lambda-rule": {"text": "An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, + "modal-web": {"text": "Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-dedicated": {"text": "Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-server": {"text": "Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-shared": {"text": "Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public.", "components": ["modal"], "sources": ["modal:s7d083f819a21", "modal:s12a4f5e04dbe", "modal:s33e1c2c2faf3"], "status": "REASONED"}, + "modal-credentials": {"text": "Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer .; missing credentials on a protected endpoint return 401.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "runpod-bind": {"text": "RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "listener-auth": {"text": "Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication.", "components": ["runpod", "vast", "jupyter", "ollama"], "sources": ["runpod:s817198f039ee", "vast:s0bdbda534723", "jupyter:sc703a8030c8f", "ollama:s96f77aee8bdd", "ollama:sd146dc5d917d", "ollama:s7478dad099ab", "ollama:s2abe15b73bce"], "status": "REASONED"}, + "mfa": {"text": "MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login.", "components": ["modal", "jupyter"], "sources": ["modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED"}, + "lambda-ssh": {"text": "Lambda requires an SSH key at launch; keep the private key off the instance.", "components": ["lambda"], "sources": ["lambda:sb12b4a953833"], "status": "REASONED"}, + "vast-ssh": {"text": "Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance.", "components": ["vast"], "sources": ["vast:s6e0303ef19fe"], "status": "REASONED"}, + "runpod-ssh": {"text": "RunPod recommends SSH keys and offers an optional password; use the key and skip the password.", "components": ["runpod"], "sources": ["runpod:s13c43b856d4f"], "status": "REASONED"}, + "verify-inventory": {"text": "This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside.", "components": ["runpod", "vast", "iproute2", "network-namespaces"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850", "iproute2:s6c87acff18cf", "iproute2:sf8fdfa16eb3f", "iproute2:s1e96518d2ede", "network-namespaces:s427c923e346f"], "status": "REASONED", "verify": [1]}, + "verify-jupyter": {"text": "This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls.", "components": ["jupyter", "jupyter-contents"], "sources": ["jupyter:sc703a8030c8f", "jupyter-contents:sdc3ad9152505", "jupyter-contents:sf0a3ea8ee42e", "jupyter-contents:s4bdc7b9183f3", "jupyter-contents:s4e7504ed1ac8", "jupyter-contents:s52316685e1e4", "jupyter-contents:s83f1081e863e", "jupyter-contents:s6c884bdf8fb4", "jupyter-contents:sb7fab0e85c94", "jupyter-contents:s58a8a77857b5"], "status": "REASONED", "verify": [1]}, + "verify-mapping": {"text": "Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control.", "components": ["runpod", "vast", "modal", "jupyter"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED", "verify": [1]}, + "verify-portal-exempt": {"text": "For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE.", "components": ["vast-image"], "sources": ["vast-image:sae1085635ff9", "vast-image:s4a05112af859", "vast-image:sd9c32e411f08", "vast-image:s3c43b58f9e41"], "status": "REASONED", "verify": [2]}, + "remote-desktop": {"text": "Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it.", "components": ["runpod", "vast", "vast-image"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast-image:s0428d55e9145", "vast-image:s1b8c6c46f358", "vast-image:s44f3439f13de"], "status": "REASONED"} + } +} +```` + diff --git a/low-code-builders.md b/low-code-builders.md index fa80f84f5..380bf29ed 100644 --- a/low-code-builders.md +++ b/low-code-builders.md @@ -1,305 +1,32 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "43cf22968b497a23408d386c38d01f763e391f115cf9a92f4fb443ce28cea9a8", - "components": { - "nocodb": { - "name": "NocoDB", - "basis": "2026.09.0", - "sources": { - "sa19c6557b712": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/Noco.ts", - "s635b35ce4689": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/trustProxy.ts", - "sd74ca4df55b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/run/dockerEntry.ts", - "s5ea55dc5f0d9": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/services/users/users.service.ts", - "sd0e9f451673f": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/interface/AppSettings.ts", - "s8a1de0bc97b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initAdminFromEnv.ts", - "sca40040b40cc": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/encryptDecrypt.ts", - "saf056bb4ff37": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initDataSourceEncryption.ts", - "sbfbf630008e1": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start.sh", - "se5da9707c684": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start-litestream.sh", - "s536899ecd4a5": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md", - "sca0c9c0ecf24": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L56-L75", - "sb4d5b659f7b8": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L77-L90", - "s2769b9054778": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L82", - "s2b16e2c93606": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L90", - "s118384fa1d9f": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L95", - "s9bee4adc612e": "https://api.github.com/repos/nocodb/nocodb/releases/tags/2026.09.0", - "s3f296c702bc5": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/package.json#L3", - "se81c30b3f43b": "https://github.com/nocodb/nocodb/blob/2026.09.0/docker-compose/examples/external-postgres-and-redis/docker.env" - } - }, - "docker": { - "name": "Docker docs", - "basis": "unknown", - "sources": { - "sd34e76a09f6c": "https://docs.docker.com/reference/cli/docker/container/exec/", - "s2fecb6db5480": "https://docs.docker.com/engine/network/firewall-iptables/" - } - }, - "docker-new": { - "name": "Docker CLI", - "basis": "v27.2.0", - "sources": { - "sca9a0e1e4b6a": "https://github.com/docker/cli/blob/v27.2.0/cli/command/formatter/container.go" - } - }, - "docker-old": { - "name": "Docker CLI", - "basis": "v27.1.2", - "sources": { - "sa202ad11d4ed": "https://github.com/docker/cli/blob/v27.1.2/cli/command/formatter/container.go" - } - }, - "node": { - "name": "Node.js", - "basis": "v22.22.1", - "sources": { - "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" - } - }, - "baserow": { - "name": "Baserow", - "basis": "2.3.4", - "sources": { - "s1b31314982f4": "https://github.com/baserow/baserow/blob/2.3.4/docker-compose.yml", - "s3f61036aca03": "https://github.com/baserow/baserow/blob/2.3.4/docs/installation/install-with-docker.md", - "s34b87f60dcc6": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/user/handler.py", - "s4588448964a2": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/models.py", - "sd858f9ee45e0": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/apps.py", - "s1a472d828699": "https://github.com/baserow/baserow/blob/2.3.4/.env.example" - } - }, - "appsmith": { - "name": "Appsmith", - "basis": "v2.4.1", - "sources": { - "s2650629933a1": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/resources/application-ce.properties", - "sd1c55be91e30": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java", - "se3cd894dc579": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java", - "sa68daf87cde7": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/fs/opt/appsmith/entrypoint.sh", - "s9fdcff5a28a4": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/docker-compose.yml", - "sfa551c344e8b": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/aws_ami/docker-compose.yml" - } - }, - "budibase": { - "name": "Budibase", - "basis": "v3.46.0", - "sources": { - "s735ead81ce9c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env", - "s74d858053d12": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml", - "s7fbf706a4c8f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf", - "s45379b35a980": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts", - "sc0a3ac85466f": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts", - "s4902d93986c7": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts", - "s5209f2549c9b": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env#L4-L16", - "s9afbeb2063b5": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L111-L120", - "s32775c45ba51": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L129-L144", - "s4d349b4d4ac3": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L86", - "s2a033b3a4117": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L236-L239", - "s2db467c4f1cf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L40-L43", - "sbd761572fef0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L133-L167", - "sb8f5a18ab4ee": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L13-L28", - "s7883460cabbf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L75-L79", - "se18a9c7182fb": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts#L229-L274", - "s816a78b83956": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/server/src/startup/index.ts#L226-L266", - "sedd5e0682bf0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/src/environment.ts#L267-L268", - "s35ae42bd7a1c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L27-L28", - "scdcd8b6e7071": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L89-L91", - "sc74f36aa8340": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L151-L154", - "sf6e2a660380f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/VERSION#L1", - "sfdc76e71aff6": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/runner.sh#L165-L166", - "s851ad716e105": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/docker-entrypoint.sh#L70-L75", - "se993218560de": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/hosting.properties#L4-L18", - "s6179ec0d9abd": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot#L11-L15", - "s9cc27222e133": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/tests/jestEnv.ts#L9-L10", - "sbe7e0469e7a8": "https://github.com/Budibase/budibase/blob/v3.46.0/scripts/dev/manage.js#L11-L12" - } - }, - "couchdb": { - "name": "CouchDB", - "basis": "3.5.2", - "sources": { - "s34292991c4ec": "https://github.com/apache/couchdb/blob/3.5.2/src/docs/src/api/server/authn.rst" - } - }, - "litellm": { - "name": "LiteLLM authentication source", - "basis": "6ef7b86748118ceecd95271727c2fa167ce55fec", - "sources": { - "sbe6fa0b81017": "https://raw.githubusercontent.com/BerriAI/litellm/6ef7b86748118ceecd95271727c2fa167ce55fec/litellm/proxy/auth/user_api_key_auth.py" - } - }, - "windmill": { - "name": "Windmill", - "basis": "v1.817.0", - "sources": { - "s91439e21d4ac": "https://github.com/windmill-labs/windmill/blob/v1.817.0/README.md", - "s8e4bbba271f2": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/src/main.rs", - "sd775b7c95872": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs", - "sfcc72cf06c9e": "https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml", - "s345ca635c837": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql", - "s03c33b490c08": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql" - } - } - }, - "claims": { - "private": {"text": "Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials.", "components": ["nocodb", "baserow", "appsmith", "budibase", "windmill"], "sources": ["nocodb:sd74ca4df55b0", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "nocodb-bind": {"text": "PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall.", "components": ["nocodb", "node"], "sources": ["nocodb:sa19c6557b712", "nocodb:sd74ca4df55b0", "node:sfb3e306c102d"], "status": "REASONED"}, - "nocodb-first": {"text": "First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:s8a1de0bc97b0"], "status": "REASONED"}, - "nocodb-signup": {"text": "invite_only_signup defaults false; enable to stop later self-signups.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, - "nocodb-encryption": {"text": "Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later.", "components": ["nocodb"], "sources": ["nocodb:sca40040b40cc", "nocodb:saf056bb4ff37"], "status": "REASONED"}, - "nocodb-jwt": {"text": "Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s536899ecd4a5"], "status": "REASONED"}, - "nocodb-cors": {"text": "Entry file allows every CORS origin.", "components": ["nocodb"], "sources": ["nocodb:sd74ca4df55b0"], "status": "REASONED"}, - "nocodb-proxy": {"text": "Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s635b35ce4689", "nocodb:sd74ca4df55b0"], "status": "REASONED"}, - "nocodb-packaging": {"text": "README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice).", "components": ["nocodb"], "sources": ["nocodb:s536899ecd4a5", "nocodb:sca0c9c0ecf24", "nocodb:sb4d5b659f7b8", "nocodb:s118384fa1d9f", "nocodb:s9bee4adc612e", "nocodb:s3f296c702bc5", "nocodb:s2769b9054778", "nocodb:s2b16e2c93606"], "status": "REASONED"}, - "baserow-bind": {"text": "Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s3f61036aca03"], "status": "REASONED"}, - "baserow-first": {"text": "First signup becomes staff; claim before exposure.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6"], "status": "REASONED"}, - "baserow-signup": {"text": "allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, - "baserow-secrets": {"text": "Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s1a472d828699"], "status": "REASONED"}, - "baserow-mfa": {"text": "Core registers a TOTP provider.", "components": ["baserow"], "sources": ["baserow:sd858f9ee45e0"], "status": "REASONED"}, - "appsmith-bind": {"text": "Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1"], "status": "REASONED"}, - "appsmith-publish": {"text": "Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4", "appsmith:sfa551c344e8b"], "status": "REASONED"}, - "appsmith-first": {"text": "First signup claims super-user while no users exist; claim before exposure.", "components": ["appsmith"], "sources": ["appsmith:sd1c55be91e30"], "status": "REASONED"}, - "appsmith-signup": {"text": "APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, - "appsmith-encryption": {"text": "Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, - "appsmith-rotation": {"text": "Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, - "budibase-publish": {"text": "Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12"], "status": "REASONED"}, - "budibase-couchdb": {"text": "Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s4d349b4d4ac3", "budibase:s2a033b3a4117", "budibase:scdcd8b6e7071", "budibase:sc74f36aa8340", "budibase:sf6e2a660380f", "budibase:sfdc76e71aff6", "budibase:s851ad716e105"], "status": "REASONED"}, - "budibase-secrets": {"text": "Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "budibase:s5209f2549c9b", "budibase:s32775c45ba51"], "status": "REASONED"}, - "budibase-sample-check": {"text": "This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s5209f2549c9b", "budibase:se993218560de", "budibase:s6179ec0d9abd", "budibase:s9cc27222e133", "budibase:sbe7e0469e7a8"], "status": "REASONED"}, - "budibase-init": {"text": "POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin.", "components": ["budibase"], "sources": ["budibase:s45379b35a980", "budibase:sc0a3ac85466f", "budibase:s4902d93986c7", "budibase:s2db467c4f1cf", "budibase:sbd761572fef0", "budibase:sb8f5a18ab4ee", "budibase:s7883460cabbf", "budibase:se18a9c7182fb"], "status": "REASONED"}, - "budibase-seed": {"text": "Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true.", "components": ["budibase"], "sources": ["budibase:sc0a3ac85466f", "budibase:sedd5e0682bf0", "budibase:s35ae42bd7a1c", "budibase:s816a78b83956"], "status": "REASONED"}, - "windmill-code": {"text": "Workers execute user scripts; a login enables code execution on worker hosts.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:sd775b7c95872"], "status": "REASONED"}, - "windmill-bind": {"text": "Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "REASONED"}, - "windmill-compose": {"text": "Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-sandbox": {"text": "DISABLE_NSJAIL defaults true; sandboxing is not enabled by default.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "REASONED"}, - "windmill-privileged": {"text": "Shipped worker uses privileged=true.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-smtp": {"text": "Port 25 serves email triggers; do not publish unless used.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-debugger": {"text": "Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "verify-nocodb-signup": {"text": "Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, - "verify-baserow-signup": {"text": "Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, - "verify-appsmith-signup": {"text": "Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, - "verify-external": {"text": "Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-default-login": {"text": "Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:s345ca635c837", "windmill:s03c33b490c08"], "status": "DEMONSTRATED", "evidence": "After the password was changed, `changeme` got `400` and the new password `200`.", "verify": [4]}, - "windmill-loopback": {"text": "SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "DEMONSTRATED", "evidence": "The server's only listener was on 127.0.0.1, at the test port the run set."}, - "windmill-sandbox-run": {"text": "Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "DEMONSTRATED", "evidence": "on the loopback run, without nsjail installed, the worker logged \"Nsjail sandboxing will NOT be available\"."}, - "verify-inventory": {"text": "Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation.", "components": ["docker", "baserow", "appsmith", "budibase", "windmill"], "sources": ["docker:s2fecb6db5480", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED", "verify": [1]}, - "verify-port-display": {"text": "IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered.", "components": ["docker", "docker-new", "docker-old"], "sources": ["docker:s2fecb6db5480", "docker-new:sca9a0e1e4b6a", "docker-old:sa202ad11d4ed"], "status": "REASONED", "verify": [1]}, - "verify-sample-tokens": {"text": "Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope.", "components": ["nocodb", "appsmith", "budibase", "windmill"], "sources": ["nocodb:s536899ecd4a5", "appsmith:s9fdcff5a28a4", "budibase:s735ead81ce9c", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On copies with the values replaced it printed \"no vendor sample tokens found\", and on an unreadable path it reported that it did not check.", "verify": [2]}, - "verify-nocodb-key": {"text": "docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified.", "components": ["nocodb", "docker"], "sources": ["nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, - "nocodb-key-limits": {"text": "Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key.", "components": ["nocodb", "docker"], "sources": ["nocodb:sca40040b40cc", "nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, - "nocodb-key-logic": {"text": "A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated.", "components": ["docker"], "sources": ["docker:sd34e76a09f6c"], "status": "DEMONSTRATED", "evidence": "With a stand-in for `docker exec`, the block printed \"non-empty\" for a set key, \"EMPTY or unset\" for an empty and an unset one, and \"not checked\" when the container was missing;"}, - "verify-windmill-control": {"text": "Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac"], "status": "REASONED"}, - "verify-couchdb": {"text": "Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control.", "components": ["budibase", "couchdb"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "couchdb:s34292991c4ec", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s2a033b3a4117", "budibase:s851ad716e105"], "status": "REASONED", "verify": [5]}, - "verify-litellm": {"text": "Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched.", "components": ["budibase", "litellm"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "litellm:sbe6fa0b81017", "budibase:s32775c45ba51"], "status": "REASONED", "verify": [5]}, - "verify-tcp-outcomes": {"text": "Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On loopback it printed \"connected\" for the control and an open port, \"timed out\" for a port whose accept queue was full (standing in for a filtered one) and \"refused\" for a closed port, and it stopped when the control was closed.", "verify": [6]}, - "verify-tcp-inputs": {"text": "Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "`::ffff:0:0` and `::ffff:7f00:1` passed and connected to a listener bound to 127.0.0.1", "verify": [6]} - } -} ---- # Low-code internal-tool builders: NocoDB, Baserow, Appsmith, Budibase, and Windmill - -**Version basis** +Written against NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0, Windmill v1.817.0, Docker CLI v27.2.0 and v27.1.2, Node.js v22.22.1, and CouchDB 3.5.2; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 51 claims: 7 demonstrated by a check that ran and 44 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-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. +These tools sit on top of your databases and APIs and let people build internal apps, forms and +automations quickly. That is also the exposure. Each one stores credentials for the systems it connects +to, and Windmill runs arbitrary code by design. Three first-run patterns recur. On a fresh instance, the +first visitor becomes the administrator. Open signup is on by default. And the vendor's own sample +configuration ships fixed secrets that too many deployments keep. Keep every one of them private +([docker.md](docker.md), [cloud-firewalls.md](cloud-firewalls.md), [tunnels.md](tunnels.md)), claim the +admin account before the instance is reachable, and front the web UI per +[fronting-auth.md](fronting-auth.md), [nginx.md](nginx.md) or [caddy.md](caddy.md). Treat the database +behind each tool as a store of every connected system's credentials ([secrets.md](secrets.md)). +Versions checked: NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0 and Windmill +v1.817.0. -Components (recorded basis): +## NocoDB -- nocodb: NocoDB (2026.09.0) -- docker: Docker docs (unknown) -- docker-new: Docker CLI (v27.2.0) -- docker-old: Docker CLI (v27.1.2) -- node: Node.js (v22.22.1) -- baserow: Baserow (2.3.4) -- appsmith: Appsmith (v2.4.1) -- budibase: Budibase (v3.46.0) -- couchdb: CouchDB (3.5.2) -- litellm: LiteLLM authentication source (6ef7b86748118ceecd95271727c2fa167ce55fec) -- windmill: Windmill (v1.817.0) +NocoDB listens on port 8080 (`process.env.PORT || '8080'`), and its entry point calls `listen` with no +host. For an omitted host, Node "will accept connections on the unspecified IPv6 address (`::`) when IPv6 +is available, or the unspecified IPv4 address (`0.0.0.0`) otherwise", so the server listens on every +interface; its code passes no host, so NocoDB offers no bind setting. Restrict it with the container +publication or a firewall. The first account to sign up gets the super-admin role, and later self-signups are allowed +unless invite-only signup is on; `invite_only_signup` defaults to `false`. Claim the admin account first, +or seed it with `NC_ADMIN_EMAIL` and `NC_ADMIN_PASSWORD`, then turn on invite-only signup. -Claims (full source mapping in the front matter): - -- private (REASONED; nocodb, baserow, appsmith, budibase, windmill): Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials. -- nocodb-bind (REASONED; nocodb, node): PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall. -- nocodb-first (REASONED; nocodb): First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD. -- nocodb-signup (REASONED; nocodb): invite_only_signup defaults false; enable to stop later self-signups. -- nocodb-encryption (REASONED; nocodb): Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later. -- nocodb-jwt (REASONED; nocodb): Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010. -- nocodb-cors (REASONED; nocodb): Entry file allows every CORS origin. -- nocodb-proxy (REASONED; nocodb): Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access. -- nocodb-packaging (REASONED; nocodb): README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice). -- baserow-bind (REASONED; baserow): Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW. -- baserow-first (REASONED; baserow): First signup becomes staff; claim before exposure. -- baserow-signup (REASONED; baserow): allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled. -- baserow-secrets (REASONED; baserow): Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own. -- baserow-mfa (REASONED; baserow): Core registers a TOTP provider. -- appsmith-bind (REASONED; appsmith): Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container. -- appsmith-publish (REASONED; appsmith): Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443. -- appsmith-first (REASONED; appsmith): First signup claims super-user while no users exist; claim before exposure. -- appsmith-signup (REASONED; appsmith): APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED. -- appsmith-encryption (REASONED; appsmith): Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start. -- appsmith-rotation (REASONED; appsmith): Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested. -- budibase-publish (REASONED; budibase): Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000. -- budibase-couchdb (REASONED; budibase): Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase. -- budibase-secrets (REASONED; budibase): Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established. -- budibase-sample-check (REASONED; budibase): This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran. -- budibase-init (REASONED; budibase): POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin. -- budibase-seed (REASONED; budibase): Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true. -- windmill-code (REASONED; windmill): Workers execute user scripts; a login enables code execution on worker hosts. -- windmill-bind (REASONED; windmill): Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed. -- windmill-compose (REASONED; windmill): Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred. -- windmill-sandbox (REASONED; windmill): DISABLE_NSJAIL defaults true; sandboxing is not enabled by default. -- windmill-privileged (REASONED; windmill): Shipped worker uses privileged=true. -- windmill-smtp (REASONED; windmill): Port 25 serves email triggers; do not publish unless used. -- windmill-debugger (REASONED; windmill): Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments. -- verify-nocodb-signup (REASONED; nocodb): Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login. -- verify-baserow-signup (REASONED; baserow): Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login. -- verify-appsmith-signup (REASONED; appsmith): Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts. -- verify-external (REASONED; baserow, budibase, windmill): Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation. -- windmill-default-login (DEMONSTRATED; windmill): Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200. -- windmill-loopback (DEMONSTRATED; windmill): SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port. -- windmill-sandbox-run (DEMONSTRATED; windmill): Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation. -- verify-inventory (REASONED; docker, baserow, appsmith, budibase, windmill): Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation. -- verify-port-display (REASONED; docker, docker-new, docker-old): IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered. -- verify-sample-tokens (DEMONSTRATED; nocodb, appsmith, budibase, windmill): Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope. -- verify-nocodb-key (REASONED; nocodb, docker): docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified. -- nocodb-key-limits (REASONED; nocodb, docker): Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key. -- nocodb-key-logic (DEMONSTRATED; docker): A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated. -- verify-windmill-control (REASONED; windmill): Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed. -- verify-couchdb (REASONED; budibase, couchdb): Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control. -- verify-litellm (REASONED; budibase, litellm): Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched. -- verify-tcp-outcomes (DEMONSTRATED; baserow, budibase, windmill): Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated. -- verify-tcp-inputs (DEMONSTRATED; baserow, budibase, windmill): Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control. - - -These tools sit on top of your databases and APIs and let people build internal apps, forms and -automations quickly. That is also the exposure. Each one stores credentials for the systems it connects -to, and Windmill runs arbitrary code by design. Three first-run patterns recur. On a fresh instance, the -first visitor becomes the administrator. Open signup is on by default. And the vendor's own sample -configuration ships fixed secrets that too many deployments keep. Keep every one of them private -([docker.md](docker.md), [cloud-firewalls.md](cloud-firewalls.md), [tunnels.md](tunnels.md)), claim the -admin account before the instance is reachable, and front the web UI per -[fronting-auth.md](fronting-auth.md), [nginx.md](nginx.md) or [caddy.md](caddy.md). Treat the database -behind each tool as a store of every connected system's credentials ([secrets.md](secrets.md)). -Versions checked: NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0 and Windmill -v1.817.0. - -## NocoDB - -NocoDB listens on port 8080 (`process.env.PORT || '8080'`), and its entry point calls `listen` with no -host. For an omitted host, Node "will accept connections on the unspecified IPv6 address (`::`) when IPv6 -is available, or the unspecified IPv4 address (`0.0.0.0`) otherwise", so the server listens on every -interface; its code passes no host, so NocoDB offers no bind setting. Restrict it with the container -publication or a firewall. The first account to sign up gets the super-admin role, and later self-signups are allowed -unless invite-only signup is on; `invite_only_signup` defaults to `false`. Claim the admin account first, -or seed it with `NC_ADMIN_EMAIL` and `NC_ADMIN_PASSWORD`, then turn on invite-only signup. - -Two secrets need your attention: +Two secrets need your attention: - **`NC_CONNECTION_ENCRYPT_KEY`.** When it is unset, the encryption helper returns the value unchanged, so the credentials of every external database you connect are stored in plaintext in NocoDB's meta @@ -735,3 +462,302 @@ mean to publish is the finding. - Windmill v1.817.0 worker (`DISABLE_NSJAIL`): https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs - Windmill v1.817.0 Compose file: https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml - Windmill v1.817.0 seeded-user migrations: https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql and https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql + + + +## 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/low-code-builders.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): + +- nocodb: NocoDB (2026.09.0) +- baserow: Baserow (2.3.4) +- appsmith: Appsmith (v2.4.1) +- budibase: Budibase (v3.46.0) +- windmill: Windmill (v1.817.0) +- docker: Docker docs (unknown) +- docker-new: Docker CLI (v27.2.0) +- docker-old: Docker CLI (v27.1.2) +- node: Node.js (v22.22.1) +- couchdb: CouchDB (3.5.2) +- litellm: LiteLLM authentication source (6ef7b86748118ceecd95271727c2fa167ce55fec) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; nocodb, baserow, appsmith, budibase, windmill): Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials. +- nocodb-bind (REASONED; nocodb, node): PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall. +- nocodb-first (REASONED; nocodb): First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD. +- nocodb-signup (REASONED; nocodb): invite_only_signup defaults false; enable to stop later self-signups. +- nocodb-encryption (REASONED; nocodb): Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later. +- nocodb-jwt (REASONED; nocodb): Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010. +- nocodb-cors (REASONED; nocodb): Entry file allows every CORS origin. +- nocodb-proxy (REASONED; nocodb): Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access. +- nocodb-packaging (REASONED; nocodb): README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice). +- baserow-bind (REASONED; baserow): Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW. +- baserow-first (REASONED; baserow): First signup becomes staff; claim before exposure. +- baserow-signup (REASONED; baserow): allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled. +- baserow-secrets (REASONED; baserow): Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own. +- baserow-mfa (REASONED; baserow): Core registers a TOTP provider. +- appsmith-bind (REASONED; appsmith): Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container. +- appsmith-publish (REASONED; appsmith): Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443. +- appsmith-first (REASONED; appsmith): First signup claims super-user while no users exist; claim before exposure. +- appsmith-signup (REASONED; appsmith): APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED. +- appsmith-encryption (REASONED; appsmith): Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start. +- appsmith-rotation (REASONED; appsmith): Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested. +- budibase-publish (REASONED; budibase): Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000. +- budibase-couchdb (REASONED; budibase): Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase. +- budibase-secrets (REASONED; budibase): Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established. +- budibase-sample-check (REASONED; budibase): This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran. +- budibase-init (REASONED; budibase): POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin. +- budibase-seed (REASONED; budibase): Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true. +- windmill-code (REASONED; windmill): Workers execute user scripts; a login enables code execution on worker hosts. +- windmill-bind (REASONED; windmill): Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed. +- windmill-compose (REASONED; windmill): Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred. +- windmill-sandbox (REASONED; windmill): DISABLE_NSJAIL defaults true; sandboxing is not enabled by default. +- windmill-privileged (REASONED; windmill): Shipped worker uses privileged=true. +- windmill-smtp (REASONED; windmill): Port 25 serves email triggers; do not publish unless used. +- windmill-debugger (REASONED; windmill): Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments. +- verify-nocodb-signup (REASONED; nocodb): Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login. +- verify-baserow-signup (REASONED; baserow): Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login. +- verify-appsmith-signup (REASONED; appsmith): Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts. +- verify-external (REASONED; baserow, budibase, windmill): Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation. +- windmill-default-login (DEMONSTRATED; windmill): Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200. +- windmill-loopback (DEMONSTRATED; windmill): SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port. +- windmill-sandbox-run (DEMONSTRATED; windmill): Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation. +- verify-inventory (REASONED; docker, baserow, appsmith, budibase, windmill): Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation. +- verify-port-display (REASONED; docker, docker-new, docker-old): IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered. +- verify-sample-tokens (DEMONSTRATED; nocodb, appsmith, budibase, windmill): Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope. +- verify-nocodb-key (REASONED; nocodb, docker): docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified. +- nocodb-key-limits (REASONED; nocodb, docker): Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key. +- nocodb-key-logic (DEMONSTRATED; docker): A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated. +- verify-windmill-control (REASONED; windmill): Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed. +- verify-couchdb (REASONED; budibase, couchdb): Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control. +- verify-litellm (REASONED; budibase, litellm): Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched. +- verify-tcp-outcomes (DEMONSTRATED; baserow, budibase, windmill): Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated. +- verify-tcp-inputs (DEMONSTRATED; baserow, budibase, windmill): Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "43cf22968b497a23408d386c38d01f763e391f115cf9a92f4fb443ce28cea9a8", + "components": { + "nocodb": { + "name": "NocoDB", + "basis": "2026.09.0", + "sources": { + "sa19c6557b712": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/Noco.ts", + "s635b35ce4689": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/trustProxy.ts", + "sd74ca4df55b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/run/dockerEntry.ts", + "s5ea55dc5f0d9": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/services/users/users.service.ts", + "sd0e9f451673f": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/interface/AppSettings.ts", + "s8a1de0bc97b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initAdminFromEnv.ts", + "sca40040b40cc": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/encryptDecrypt.ts", + "saf056bb4ff37": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initDataSourceEncryption.ts", + "sbfbf630008e1": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start.sh", + "se5da9707c684": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start-litestream.sh", + "s536899ecd4a5": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md", + "sca0c9c0ecf24": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L56-L75", + "sb4d5b659f7b8": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L77-L90", + "s2769b9054778": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L82", + "s2b16e2c93606": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L90", + "s118384fa1d9f": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L95", + "s9bee4adc612e": "https://api.github.com/repos/nocodb/nocodb/releases/tags/2026.09.0", + "s3f296c702bc5": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/package.json#L3", + "se81c30b3f43b": "https://github.com/nocodb/nocodb/blob/2026.09.0/docker-compose/examples/external-postgres-and-redis/docker.env" + } + }, + "baserow": { + "name": "Baserow", + "basis": "2.3.4", + "sources": { + "s1b31314982f4": "https://github.com/baserow/baserow/blob/2.3.4/docker-compose.yml", + "s3f61036aca03": "https://github.com/baserow/baserow/blob/2.3.4/docs/installation/install-with-docker.md", + "s34b87f60dcc6": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/user/handler.py", + "s4588448964a2": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/models.py", + "sd858f9ee45e0": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/apps.py", + "s1a472d828699": "https://github.com/baserow/baserow/blob/2.3.4/.env.example" + } + }, + "appsmith": { + "name": "Appsmith", + "basis": "v2.4.1", + "sources": { + "s2650629933a1": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/resources/application-ce.properties", + "sd1c55be91e30": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java", + "se3cd894dc579": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java", + "sa68daf87cde7": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/fs/opt/appsmith/entrypoint.sh", + "s9fdcff5a28a4": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/docker-compose.yml", + "sfa551c344e8b": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/aws_ami/docker-compose.yml" + } + }, + "budibase": { + "name": "Budibase", + "basis": "v3.46.0", + "sources": { + "s735ead81ce9c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env", + "s74d858053d12": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml", + "s7fbf706a4c8f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf", + "s45379b35a980": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts", + "sc0a3ac85466f": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts", + "s4902d93986c7": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts", + "s5209f2549c9b": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env#L4-L16", + "s9afbeb2063b5": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L111-L120", + "s32775c45ba51": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L129-L144", + "s4d349b4d4ac3": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L86", + "s2a033b3a4117": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L236-L239", + "s2db467c4f1cf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L40-L43", + "sbd761572fef0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L133-L167", + "sb8f5a18ab4ee": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L13-L28", + "s7883460cabbf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L75-L79", + "se18a9c7182fb": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts#L229-L274", + "s816a78b83956": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/server/src/startup/index.ts#L226-L266", + "sedd5e0682bf0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/src/environment.ts#L267-L268", + "s35ae42bd7a1c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L27-L28", + "scdcd8b6e7071": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L89-L91", + "sc74f36aa8340": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L151-L154", + "sf6e2a660380f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/VERSION#L1", + "sfdc76e71aff6": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/runner.sh#L165-L166", + "s851ad716e105": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/docker-entrypoint.sh#L70-L75", + "se993218560de": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/hosting.properties#L4-L18", + "s6179ec0d9abd": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot#L11-L15", + "s9cc27222e133": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/tests/jestEnv.ts#L9-L10", + "sbe7e0469e7a8": "https://github.com/Budibase/budibase/blob/v3.46.0/scripts/dev/manage.js#L11-L12" + } + }, + "windmill": { + "name": "Windmill", + "basis": "v1.817.0", + "sources": { + "s91439e21d4ac": "https://github.com/windmill-labs/windmill/blob/v1.817.0/README.md", + "s8e4bbba271f2": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/src/main.rs", + "sd775b7c95872": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs", + "sfcc72cf06c9e": "https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml", + "s345ca635c837": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql", + "s03c33b490c08": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql" + } + }, + "docker": { + "name": "Docker docs", + "basis": "unknown", + "sources": { + "sd34e76a09f6c": "https://docs.docker.com/reference/cli/docker/container/exec/", + "s2fecb6db5480": "https://docs.docker.com/engine/network/firewall-iptables/" + } + }, + "docker-new": { + "name": "Docker CLI", + "basis": "v27.2.0", + "sources": { + "sca9a0e1e4b6a": "https://github.com/docker/cli/blob/v27.2.0/cli/command/formatter/container.go" + } + }, + "docker-old": { + "name": "Docker CLI", + "basis": "v27.1.2", + "sources": { + "sa202ad11d4ed": "https://github.com/docker/cli/blob/v27.1.2/cli/command/formatter/container.go" + } + }, + "node": { + "name": "Node.js", + "basis": "v22.22.1", + "sources": { + "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" + } + }, + "couchdb": { + "name": "CouchDB", + "basis": "3.5.2", + "sources": { + "s34292991c4ec": "https://github.com/apache/couchdb/blob/3.5.2/src/docs/src/api/server/authn.rst" + } + }, + "litellm": { + "name": "LiteLLM authentication source", + "basis": "6ef7b86748118ceecd95271727c2fa167ce55fec", + "sources": { + "sbe6fa0b81017": "https://raw.githubusercontent.com/BerriAI/litellm/6ef7b86748118ceecd95271727c2fa167ce55fec/litellm/proxy/auth/user_api_key_auth.py" + } + } + }, + "claims": { + "private": {"text": "Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials.", "components": ["nocodb", "baserow", "appsmith", "budibase", "windmill"], "sources": ["nocodb:sd74ca4df55b0", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "nocodb-bind": {"text": "PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall.", "components": ["nocodb", "node"], "sources": ["nocodb:sa19c6557b712", "nocodb:sd74ca4df55b0", "node:sfb3e306c102d"], "status": "REASONED"}, + "nocodb-first": {"text": "First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:s8a1de0bc97b0"], "status": "REASONED"}, + "nocodb-signup": {"text": "invite_only_signup defaults false; enable to stop later self-signups.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, + "nocodb-encryption": {"text": "Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later.", "components": ["nocodb"], "sources": ["nocodb:sca40040b40cc", "nocodb:saf056bb4ff37"], "status": "REASONED"}, + "nocodb-jwt": {"text": "Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s536899ecd4a5"], "status": "REASONED"}, + "nocodb-cors": {"text": "Entry file allows every CORS origin.", "components": ["nocodb"], "sources": ["nocodb:sd74ca4df55b0"], "status": "REASONED"}, + "nocodb-proxy": {"text": "Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s635b35ce4689", "nocodb:sd74ca4df55b0"], "status": "REASONED"}, + "nocodb-packaging": {"text": "README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice).", "components": ["nocodb"], "sources": ["nocodb:s536899ecd4a5", "nocodb:sca0c9c0ecf24", "nocodb:sb4d5b659f7b8", "nocodb:s118384fa1d9f", "nocodb:s9bee4adc612e", "nocodb:s3f296c702bc5", "nocodb:s2769b9054778", "nocodb:s2b16e2c93606"], "status": "REASONED"}, + "baserow-bind": {"text": "Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s3f61036aca03"], "status": "REASONED"}, + "baserow-first": {"text": "First signup becomes staff; claim before exposure.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6"], "status": "REASONED"}, + "baserow-signup": {"text": "allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, + "baserow-secrets": {"text": "Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s1a472d828699"], "status": "REASONED"}, + "baserow-mfa": {"text": "Core registers a TOTP provider.", "components": ["baserow"], "sources": ["baserow:sd858f9ee45e0"], "status": "REASONED"}, + "appsmith-bind": {"text": "Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1"], "status": "REASONED"}, + "appsmith-publish": {"text": "Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4", "appsmith:sfa551c344e8b"], "status": "REASONED"}, + "appsmith-first": {"text": "First signup claims super-user while no users exist; claim before exposure.", "components": ["appsmith"], "sources": ["appsmith:sd1c55be91e30"], "status": "REASONED"}, + "appsmith-signup": {"text": "APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, + "appsmith-encryption": {"text": "Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, + "appsmith-rotation": {"text": "Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, + "budibase-publish": {"text": "Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12"], "status": "REASONED"}, + "budibase-couchdb": {"text": "Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s4d349b4d4ac3", "budibase:s2a033b3a4117", "budibase:scdcd8b6e7071", "budibase:sc74f36aa8340", "budibase:sf6e2a660380f", "budibase:sfdc76e71aff6", "budibase:s851ad716e105"], "status": "REASONED"}, + "budibase-secrets": {"text": "Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "budibase:s5209f2549c9b", "budibase:s32775c45ba51"], "status": "REASONED"}, + "budibase-sample-check": {"text": "This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s5209f2549c9b", "budibase:se993218560de", "budibase:s6179ec0d9abd", "budibase:s9cc27222e133", "budibase:sbe7e0469e7a8"], "status": "REASONED"}, + "budibase-init": {"text": "POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin.", "components": ["budibase"], "sources": ["budibase:s45379b35a980", "budibase:sc0a3ac85466f", "budibase:s4902d93986c7", "budibase:s2db467c4f1cf", "budibase:sbd761572fef0", "budibase:sb8f5a18ab4ee", "budibase:s7883460cabbf", "budibase:se18a9c7182fb"], "status": "REASONED"}, + "budibase-seed": {"text": "Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true.", "components": ["budibase"], "sources": ["budibase:sc0a3ac85466f", "budibase:sedd5e0682bf0", "budibase:s35ae42bd7a1c", "budibase:s816a78b83956"], "status": "REASONED"}, + "windmill-code": {"text": "Workers execute user scripts; a login enables code execution on worker hosts.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:sd775b7c95872"], "status": "REASONED"}, + "windmill-bind": {"text": "Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "REASONED"}, + "windmill-compose": {"text": "Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-sandbox": {"text": "DISABLE_NSJAIL defaults true; sandboxing is not enabled by default.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "REASONED"}, + "windmill-privileged": {"text": "Shipped worker uses privileged=true.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-smtp": {"text": "Port 25 serves email triggers; do not publish unless used.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-debugger": {"text": "Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "verify-nocodb-signup": {"text": "Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, + "verify-baserow-signup": {"text": "Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, + "verify-appsmith-signup": {"text": "Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, + "verify-external": {"text": "Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-default-login": {"text": "Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:s345ca635c837", "windmill:s03c33b490c08"], "status": "DEMONSTRATED", "evidence": "After the password was changed, `changeme` got `400` and the new password `200`.", "verify": [4]}, + "windmill-loopback": {"text": "SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "DEMONSTRATED", "evidence": "The server's only listener was on 127.0.0.1, at the test port the run set."}, + "windmill-sandbox-run": {"text": "Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "DEMONSTRATED", "evidence": "on the loopback run, without nsjail installed, the worker logged \"Nsjail sandboxing will NOT be available\"."}, + "verify-inventory": {"text": "Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation.", "components": ["docker", "baserow", "appsmith", "budibase", "windmill"], "sources": ["docker:s2fecb6db5480", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED", "verify": [1]}, + "verify-port-display": {"text": "IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered.", "components": ["docker", "docker-new", "docker-old"], "sources": ["docker:s2fecb6db5480", "docker-new:sca9a0e1e4b6a", "docker-old:sa202ad11d4ed"], "status": "REASONED", "verify": [1]}, + "verify-sample-tokens": {"text": "Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope.", "components": ["nocodb", "appsmith", "budibase", "windmill"], "sources": ["nocodb:s536899ecd4a5", "appsmith:s9fdcff5a28a4", "budibase:s735ead81ce9c", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On copies with the values replaced it printed \"no vendor sample tokens found\", and on an unreadable path it reported that it did not check.", "verify": [2]}, + "verify-nocodb-key": {"text": "docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified.", "components": ["nocodb", "docker"], "sources": ["nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, + "nocodb-key-limits": {"text": "Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key.", "components": ["nocodb", "docker"], "sources": ["nocodb:sca40040b40cc", "nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, + "nocodb-key-logic": {"text": "A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated.", "components": ["docker"], "sources": ["docker:sd34e76a09f6c"], "status": "DEMONSTRATED", "evidence": "With a stand-in for `docker exec`, the block printed \"non-empty\" for a set key, \"EMPTY or unset\" for an empty and an unset one, and \"not checked\" when the container was missing;"}, + "verify-windmill-control": {"text": "Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac"], "status": "REASONED"}, + "verify-couchdb": {"text": "Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control.", "components": ["budibase", "couchdb"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "couchdb:s34292991c4ec", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s2a033b3a4117", "budibase:s851ad716e105"], "status": "REASONED", "verify": [5]}, + "verify-litellm": {"text": "Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched.", "components": ["budibase", "litellm"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "litellm:sbe6fa0b81017", "budibase:s32775c45ba51"], "status": "REASONED", "verify": [5]}, + "verify-tcp-outcomes": {"text": "Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On loopback it printed \"connected\" for the control and an open port, \"timed out\" for a port whose accept queue was full (standing in for a filtered one) and \"refused\" for a closed port, and it stopped when the control was closed.", "verify": [6]}, + "verify-tcp-inputs": {"text": "Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "`::ffff:0:0` and `::ffff:7f00:1` passed and connected to a listener bound to 127.0.0.1", "verify": [6]} + } +} +```` + diff --git a/mfa.md b/mfa.md index a45f3bce0..fb2c52b2c 100644 --- a/mfa.md +++ b/mfa.md @@ -1,6 +1,6 @@ # Multi-factor authentication (MFA) -Written against TOTP standard (no recorded version), Caddy forward_auth minimum 2.5.1, google-authenticator-libpam source 1.11, MySQL 8.0 multifactor authentication 8.0, MySQL 8.4 multifactor authentication 8.4, MySQL 8.4 WebAuthn authentication 8.4, and authentik source version/2026.8.3, plus 10 more recorded bases; 14 further sources are pinned to a commit and 13 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 35 claims: 2 demonstrated by a check that ran and 33 reasoned from cited sources. +Written against TOTP standard (no recorded version), authentik source version/2026.8.3, Keycloak documentation 26.7.4, oauth2-proxy documentation v7.15.4, Caddy forward_auth minimum 2.5.1, google-authenticator-libpam source 1.11, and MySQL 8.0 multifactor authentication 8.0, plus 10 more recorded bases; 14 further sources are pinned to a commit and 13 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 35 claims: 2 demonstrated by a check that ran and 33 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. @@ -171,6 +171,9 @@ Components (recorded basis): - uri: Authenticator key URI format (unknown) - webauthn: WebAuthn standard (unknown) - authelia: Authelia proxy support (unknown) +- authentik-source: authentik source (version/2026.8.3) +- keycloak-source: Keycloak documentation (26.7.4) +- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) - caddy-min: Caddy forward_auth minimum (2.5.1) - duo: Duo editions (unknown) - duo-push-policy: Duo Verified Push policy (unknown) @@ -184,9 +187,6 @@ Components (recorded basis): - mysql: MySQL 8.0 multifactor authentication (8.0) - mysql-8-4: MySQL 8.4 multifactor authentication (8.4) - mysql-webauthn: MySQL 8.4 WebAuthn authentication (8.4) -- authentik-source: authentik source (version/2026.8.3) -- keycloak-source: Keycloak documentation (26.7.4) -- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) - entra-docs: Microsoft Entra documentation (231747abc59ae3d50c74e215c1cdd592eb980723) - owasp-docs: OWASP cheat sheets (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) - otp-source: pquerna/otp source (617f8d5f3f518bae0f5c3ec14a17ca19f2a6f870) @@ -291,6 +291,28 @@ Claims (full source mapping in the JSON block below): "s1409ac8fe3d9": "https://www.authelia.com/integration/proxies/support/" } }, + "authentik-source": { + "name": "authentik source", + "basis": "version/2026.8.3", + "sources": { + "s18275a7072e9": "https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/stages/authenticator_validate/stage.py#L80-L108" + } + }, + "keycloak-source": { + "name": "Keycloak documentation", + "basis": "26.7.4", + "sources": { + "s1e55f7daaa0d": "https://github.com/keycloak/keycloak/blob/26.7.4/docs/documentation/server_admin/topics/authentication/otp-policies.adoc#L2-L21" + } + }, + "oauth2-proxy-source": { + "name": "oauth2-proxy documentation", + "basis": "v7.15.4", + "sources": { + "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", + "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" + } + }, "caddy-min": { "name": "Caddy forward_auth minimum", "basis": "2.5.1", @@ -385,28 +407,6 @@ Claims (full source mapping in the JSON block below): "s807a77fa8872": "https://dev.mysql.com/doc/refman/8.4/en/webauthn-pluggable-authentication.html" } }, - "authentik-source": { - "name": "authentik source", - "basis": "version/2026.8.3", - "sources": { - "s18275a7072e9": "https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/stages/authenticator_validate/stage.py#L80-L108" - } - }, - "keycloak-source": { - "name": "Keycloak documentation", - "basis": "26.7.4", - "sources": { - "s1e55f7daaa0d": "https://github.com/keycloak/keycloak/blob/26.7.4/docs/documentation/server_admin/topics/authentication/otp-policies.adoc#L2-L21" - } - }, - "oauth2-proxy-source": { - "name": "oauth2-proxy documentation", - "basis": "v7.15.4", - "sources": { - "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", - "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" - } - }, "entra-docs": { "name": "Microsoft Entra documentation", "basis": "231747abc59ae3d50c74e215c1cdd592eb980723", diff --git a/mlflow.md b/mlflow.md index e394238f2..5d125755f 100644 --- a/mlflow.md +++ b/mlflow.md @@ -1,204 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "ee30ff570fece0f829e8b7a87b1a777387f0b1f32d2e160049713119d344088e", - "components": { - "docs": { - "name": "MLflow documentation", - "basis": "unknown", - "sources": { - "scbbc07258943": "https://mlflow.org/docs/latest/self-hosting/security/basic-http-auth/", - "sc3a2d70cabf9": "https://mlflow.org/docs/latest/api_reference/auth/rest-api.html", - "s5b79d1871e0b": "https://mlflow.org/docs/latest/api_reference/cli.html", - "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server", - "s996e587e947a": "https://mlflow.org/docs/latest/api_reference/rest-api.html", - "s559e7a8271b9": "https://mlflow.org/docs/latest/api_reference/cli.html#mlflow" - } - }, - "source": { - "name": "MLflow pinned source", - "basis": "v3.16.1", - "sources": { - "sf7285d252e29": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/cli_args.py#L162-L180", - "sa3993a9b625f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L369-L541", - "sd775f10c4d07": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/auth/__init__.py#L6008-L6021", - "s349a81b9478e": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/__init__.py#L401-L402", - "s99d30948902f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L85-L86", - "s687f1c9977cd": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L94-L105", - "s1661739df39d": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/environment_variables.py#L493-L503", - "s5801c6ac1ac2": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/rest_utils.py#L254-L292" - } - }, - "middleware-min": { - "name": "MLflow security middleware minimum", - "basis": "3.5.0", - "sources": { - "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server" - } - }, - "docker": { - "name": "Docker Engine loopback publication minimum", - "basis": "28.0.0", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - }, - "curl": { - "name": "curl minimum write-out version", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "nginx": { - "name": "nginx proxy implementation", - "basis": "release-1.28.0", - "sources": { - "s6dfa81e36717": "https://github.com/nginx/nginx/blob/release-1.28.0/src/http/modules/ngx_http_proxy_module.c#L305-L310" - } - }, - "caddy": { - "name": "Caddy reverse proxy parser", - "basis": "v2.11.4", - "sources": { - "s52039756d4f4": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L40", - "s9dde4a2bda4c": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L58-L62" - } - }, - "iproute2": { - "name": "iproute2 ss manual", - "basis": "v6.12.0", - "sources": { - "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", - "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", - "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", - "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375" - } - }, - "cf-docs": { - "name": "Cloudflare 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", - "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", - "sbec4996a7089": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33", - "s543b1f2eb29c": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49", - "sc565f2d80e6e": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59" - } - }, - "oauth2-proxy-source": { - "name": "oauth2-proxy documentation", - "basis": "v7.15.4", - "sources": { - "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", - "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" - } - }, - "authelia-source": { - "name": "Authelia source", - "basis": "v4.39.28", - "sources": { - "s768b165b44a0": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507", - "sd4dd76e70475": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537" - } - } - }, - "claims": { - "default-host": {"text": "v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, - "default-port": {"text": "v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, - "default-auth": {"text": "Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s2517159af609"], "status": "REASONED"}, - "container-bind": {"text": "Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports.", "components": ["docker", "docs"], "sources": ["docker:s1e53417c513d", "docs:s5b79d1871e0b"], "status": "REASONED"}, - "middleware-scope": {"text": "Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it.", "components": ["middleware-min", "docs"], "sources": ["middleware-min:s2517159af609", "docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "host-allowlist": {"text": "Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "cors": {"text": "Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "middleware-disable": {"text": "Do not use --disable-security-middleware outside a test.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "tls": {"text": "There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname.", "components": ["docs", "nginx", "caddy"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609", "nginx:s6dfa81e36717", "caddy:s52039756d4f4", "caddy:s9dde4a2bda4c"], "status": "REASONED"}, - "client-tls": {"text": "Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, - "auth-app": {"text": "Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "csrf-key": {"text": "Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "env-file": {"text": "Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching.", "components": ["docs", "source"], "sources": ["docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, - "key-exposure": {"text": "Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure.", "components": ["source", "docs"], "sources": ["source:sd775f10c4d07", "source:s349a81b9478e", "docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, - "admin-bootstrap": {"text": "No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "default-permission": {"text": "default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "auth-database": {"text": "database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "auth-config": {"text": "MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "password-rotation": {"text": "PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping.", "components": ["docs", "curl"], "sources": ["docs:sc3a2d70cabf9", "curl:s2b2686afaf41"], "status": "REASONED"}, - "user-creation": {"text": "User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:sc3a2d70cabf9"], "status": "REASONED"}, - "client-credentials": {"text": "~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "rate-limit": {"text": "The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "mfa": {"text": "MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination.", "components": ["docs", "cf-docs", "oauth2-proxy-source", "authelia-source"], "sources": ["docs:scbbc07258943", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "oauth2-proxy-source:sa403e3d826b4", "oauth2-proxy-source:se7a200583def", "authelia-source:s768b165b44a0", "authelia-source:sd4dd76e70475"], "status": "REASONED"}, - "proxy-token": {"text": "MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together.", "components": ["docs", "source", "cf-docs"], "sources": ["docs:s2517159af609", "source:s1661739df39d", "source:s5801c6ac1ac2", "cf-docs:sbec4996a7089", "cf-docs:s543b1f2eb29c", "cf-docs:sc565f2d80e6e"], "status": "REASONED"}, - "artifact-proxy": {"text": "--serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "artifact-existing": {"text": "Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, - "artifact-direct": {"text": "With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, - "verify-bind": {"text": "Read every listener and require 5000 on 127.0.0.1.", "components": ["source", "iproute2"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850"], "status": "REASONED", "verify": [1]}, - "verify-network": {"text": "Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive.", "components": ["curl", "docs"], "sources": ["curl:s2b2686afaf41", "docs:s5b79d1871e0b"], "status": "REASONED", "verify": [1]}, - "verify-public-auth": {"text": "POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass.", "components": ["docs", "curl"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-backend-auth": {"text": "Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s5b79d1871e0b", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]}, - "verify-authorization": {"text": "Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]} - } -} ---- # MLflow tracking server: no authentication by default - -**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): - -- docs: MLflow documentation (unknown) -- source: MLflow pinned source (v3.16.1) -- middleware-min: MLflow security middleware minimum (3.5.0) -- docker: Docker Engine loopback publication minimum (28.0.0) -- curl: curl minimum write-out version (7.75.0) -- nginx: nginx proxy implementation (release-1.28.0) -- caddy: Caddy reverse proxy parser (v2.11.4) -- iproute2: iproute2 ss manual (v6.12.0) -- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) -- authelia-source: Authelia source (v4.39.28) +Written against MLflow pinned source v3.16.1, MLflow security middleware minimum 3.5.0, Docker Engine loopback publication minimum 28.0.0, nginx proxy implementation release-1.28.0, Caddy reverse proxy parser v2.11.4, oauth2-proxy documentation v7.15.4, and Authelia source v4.39.28, plus 2 more recorded bases; 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 32 claims, all reasoned from cited sources. -Claims (full source mapping in the front matter): - -- default-host (REASONED; source): v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting. -- default-port (REASONED; source): v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it. -- default-auth (REASONED; docs): Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts. -- container-bind (REASONED; docker, docs): Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports. -- middleware-scope (REASONED; middleware-min, docs): Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it. -- host-allowlist (REASONED; docs): Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules. -- cors (REASONED; docs): Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin. -- middleware-disable (REASONED; docs): Do not use --disable-security-middleware outside a test. -- tls (REASONED; docs, nginx, caddy): There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname. -- client-tls (REASONED; docs): Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production. -- auth-app (REASONED; docs): Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label. -- csrf-key (REASONED; docs): Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control. -- env-file (REASONED; docs, source): Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching. -- key-exposure (REASONED; source, docs): Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure. -- admin-bootstrap (REASONED; docs): No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply. -- default-permission (REASONED; docs): default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant. -- auth-database (REASONED; docs): database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example. -- auth-config (REASONED; docs): MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic. -- password-rotation (REASONED; docs, curl): PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping. -- user-creation (REASONED; docs): User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user. -- client-credentials (REASONED; docs): ~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD. -- rate-limit (REASONED; docs): The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS. -- mfa (REASONED; docs, cf-docs, oauth2-proxy-source, authelia-source): MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination. -- proxy-token (REASONED; docs, source, cf-docs): MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together. -- artifact-proxy (REASONED; docs): --serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none. -- artifact-existing (REASONED; docs): Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly. -- artifact-direct (REASONED; docs): With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets. -- verify-bind (REASONED; source, iproute2): Read every listener and require 5000 on 127.0.0.1. -- verify-network (REASONED; curl, docs): Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive. -- verify-public-auth (REASONED; docs, curl): POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass. -- verify-backend-auth (REASONED; docs): Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement. -- verify-authorization (REASONED; docs): Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is 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. `mlflow server` serves the tracking UI and REST API at `http://127.0.0.1:5000` (as of v3.16.1, unless `MLFLOW_HOST` or `MLFLOW_PORT` is set) and performs no authentication: anyone who can reach the port can read, alter, and delete experiments, runs, registered models, and (with artifact proxying on) the artifacts themselves. Authentication is opt-in through a separate app, and the server has no dedicated TLS flags (though `mlflow server --uvicorn-opts` can forward `--ssl-keyfile`/`--ssl-certfile` to the default Uvicorn server); MLflow's tracking-server documentation recommends a reverse proxy or VPN for both. @@ -394,3 +198,225 @@ REASONED: preceding block; listener inventory, external TCP reachability, proxy/ - Cloudflare service-token Service Auth requirement, request headers and configurable single-header alternative (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33, https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49, https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59 - oauth2-proxy v7.15.4 provider and upstream configuration; these entries do not establish MFA enforcement (checked October 2026): https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108, https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274 - Authelia v4.39.28 first matching rule, default policy and two_factor requirement (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507, https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537 + + + +## 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/mlflow.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): + +- source: MLflow pinned source (v3.16.1) +- middleware-min: MLflow security middleware minimum (3.5.0) +- docs: MLflow documentation (unknown) +- docker: Docker Engine loopback publication minimum (28.0.0) +- nginx: nginx proxy implementation (release-1.28.0) +- caddy: Caddy reverse proxy parser (v2.11.4) +- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) +- authelia-source: Authelia source (v4.39.28) +- curl: curl minimum write-out version (7.75.0) +- iproute2: iproute2 ss manual (v6.12.0) + +Claims (full source mapping in the JSON block below): + +- default-host (REASONED; source): v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting. +- default-port (REASONED; source): v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it. +- default-auth (REASONED; docs): Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts. +- container-bind (REASONED; docker, docs): Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports. +- middleware-scope (REASONED; middleware-min, docs): Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it. +- host-allowlist (REASONED; docs): Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules. +- cors (REASONED; docs): Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin. +- middleware-disable (REASONED; docs): Do not use --disable-security-middleware outside a test. +- tls (REASONED; docs, nginx, caddy): There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname. +- client-tls (REASONED; docs): Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production. +- auth-app (REASONED; docs): Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label. +- csrf-key (REASONED; docs): Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control. +- env-file (REASONED; docs, source): Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching. +- key-exposure (REASONED; source, docs): Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure. +- admin-bootstrap (REASONED; docs): No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply. +- default-permission (REASONED; docs): default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant. +- auth-database (REASONED; docs): database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example. +- auth-config (REASONED; docs): MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic. +- password-rotation (REASONED; docs, curl): PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping. +- user-creation (REASONED; docs): User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user. +- client-credentials (REASONED; docs): ~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD. +- rate-limit (REASONED; docs): The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS. +- mfa (REASONED; docs, cf-docs, oauth2-proxy-source, authelia-source): MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination. +- proxy-token (REASONED; docs, source, cf-docs): MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together. +- artifact-proxy (REASONED; docs): --serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none. +- artifact-existing (REASONED; docs): Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly. +- artifact-direct (REASONED; docs): With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets. +- verify-bind (REASONED; source, iproute2): Read every listener and require 5000 on 127.0.0.1. +- verify-network (REASONED; curl, docs): Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive. +- verify-public-auth (REASONED; docs, curl): POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass. +- verify-backend-auth (REASONED; docs): Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement. +- verify-authorization (REASONED; docs): Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "ee30ff570fece0f829e8b7a87b1a777387f0b1f32d2e160049713119d344088e", + "components": { + "source": { + "name": "MLflow pinned source", + "basis": "v3.16.1", + "sources": { + "sf7285d252e29": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/cli_args.py#L162-L180", + "sa3993a9b625f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L369-L541", + "sd775f10c4d07": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/auth/__init__.py#L6008-L6021", + "s349a81b9478e": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/__init__.py#L401-L402", + "s99d30948902f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L85-L86", + "s687f1c9977cd": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L94-L105", + "s1661739df39d": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/environment_variables.py#L493-L503", + "s5801c6ac1ac2": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/rest_utils.py#L254-L292" + } + }, + "middleware-min": { + "name": "MLflow security middleware minimum", + "basis": "3.5.0", + "sources": { + "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server" + } + }, + "docs": { + "name": "MLflow documentation", + "basis": "unknown", + "sources": { + "scbbc07258943": "https://mlflow.org/docs/latest/self-hosting/security/basic-http-auth/", + "sc3a2d70cabf9": "https://mlflow.org/docs/latest/api_reference/auth/rest-api.html", + "s5b79d1871e0b": "https://mlflow.org/docs/latest/api_reference/cli.html", + "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server", + "s996e587e947a": "https://mlflow.org/docs/latest/api_reference/rest-api.html", + "s559e7a8271b9": "https://mlflow.org/docs/latest/api_reference/cli.html#mlflow" + } + }, + "docker": { + "name": "Docker Engine loopback publication minimum", + "basis": "28.0.0", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "nginx": { + "name": "nginx proxy implementation", + "basis": "release-1.28.0", + "sources": { + "s6dfa81e36717": "https://github.com/nginx/nginx/blob/release-1.28.0/src/http/modules/ngx_http_proxy_module.c#L305-L310" + } + }, + "caddy": { + "name": "Caddy reverse proxy parser", + "basis": "v2.11.4", + "sources": { + "s52039756d4f4": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L40", + "s9dde4a2bda4c": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L58-L62" + } + }, + "cf-docs": { + "name": "Cloudflare 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", + "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", + "sbec4996a7089": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33", + "s543b1f2eb29c": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49", + "sc565f2d80e6e": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59" + } + }, + "oauth2-proxy-source": { + "name": "oauth2-proxy documentation", + "basis": "v7.15.4", + "sources": { + "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", + "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" + } + }, + "authelia-source": { + "name": "Authelia source", + "basis": "v4.39.28", + "sources": { + "s768b165b44a0": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507", + "sd4dd76e70475": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.12.0", + "sources": { + "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", + "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", + "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", + "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375" + } + } + }, + "claims": { + "default-host": {"text": "v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, + "default-port": {"text": "v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, + "default-auth": {"text": "Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s2517159af609"], "status": "REASONED"}, + "container-bind": {"text": "Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports.", "components": ["docker", "docs"], "sources": ["docker:s1e53417c513d", "docs:s5b79d1871e0b"], "status": "REASONED"}, + "middleware-scope": {"text": "Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it.", "components": ["middleware-min", "docs"], "sources": ["middleware-min:s2517159af609", "docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "host-allowlist": {"text": "Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "cors": {"text": "Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "middleware-disable": {"text": "Do not use --disable-security-middleware outside a test.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "tls": {"text": "There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname.", "components": ["docs", "nginx", "caddy"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609", "nginx:s6dfa81e36717", "caddy:s52039756d4f4", "caddy:s9dde4a2bda4c"], "status": "REASONED"}, + "client-tls": {"text": "Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "auth-app": {"text": "Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "csrf-key": {"text": "Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "env-file": {"text": "Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching.", "components": ["docs", "source"], "sources": ["docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, + "key-exposure": {"text": "Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure.", "components": ["source", "docs"], "sources": ["source:sd775f10c4d07", "source:s349a81b9478e", "docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, + "admin-bootstrap": {"text": "No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "default-permission": {"text": "default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "auth-database": {"text": "database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "auth-config": {"text": "MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "password-rotation": {"text": "PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping.", "components": ["docs", "curl"], "sources": ["docs:sc3a2d70cabf9", "curl:s2b2686afaf41"], "status": "REASONED"}, + "user-creation": {"text": "User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:sc3a2d70cabf9"], "status": "REASONED"}, + "client-credentials": {"text": "~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "rate-limit": {"text": "The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "mfa": {"text": "MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination.", "components": ["docs", "cf-docs", "oauth2-proxy-source", "authelia-source"], "sources": ["docs:scbbc07258943", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "oauth2-proxy-source:sa403e3d826b4", "oauth2-proxy-source:se7a200583def", "authelia-source:s768b165b44a0", "authelia-source:sd4dd76e70475"], "status": "REASONED"}, + "proxy-token": {"text": "MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together.", "components": ["docs", "source", "cf-docs"], "sources": ["docs:s2517159af609", "source:s1661739df39d", "source:s5801c6ac1ac2", "cf-docs:sbec4996a7089", "cf-docs:s543b1f2eb29c", "cf-docs:sc565f2d80e6e"], "status": "REASONED"}, + "artifact-proxy": {"text": "--serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "artifact-existing": {"text": "Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "artifact-direct": {"text": "With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "verify-bind": {"text": "Read every listener and require 5000 on 127.0.0.1.", "components": ["source", "iproute2"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850"], "status": "REASONED", "verify": [1]}, + "verify-network": {"text": "Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive.", "components": ["curl", "docs"], "sources": ["curl:s2b2686afaf41", "docs:s5b79d1871e0b"], "status": "REASONED", "verify": [1]}, + "verify-public-auth": {"text": "POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass.", "components": ["docs", "curl"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-backend-auth": {"text": "Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s5b79d1871e0b", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]}, + "verify-authorization": {"text": "Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/nextjs.md b/nextjs.md index 90f1b8e6d..eea3b0a01 100644 --- a/nextjs.md +++ b/nextjs.md @@ -1,187 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "3bdad54327114894a6f45d5d5adb334b0b4fbc9bf784629b0385e6e2c4063461", - "components": { - "next": { - "name": "Next.js documentation", - "basis": "unknown", - "sources": { - "s066f8a176a52": "https://nextjs.org/docs/app/api-reference/cli/next", - "scd5749a71a79": "https://nextjs.org/docs/app/api-reference/file-conventions/proxy", - "sc2a39ce10fe1": "https://nextjs.org/docs/app/api-reference/file-conventions/route", - "sfbc4364fb5cc": "https://nextjs.org/docs/app/api-reference/functions/cookies", - "sa45045bebccb": "https://nextjs.org/docs/app/guides/authentication", - "s23a087193901": "https://nextjs.org/docs/app/guides/data-security", - "s964651d4eed4": "https://nextjs.org/docs/app/guides/environment-variables", - "s353c30dff99f": "https://nextjs.org/docs/app/guides/self-hosting" - } - }, - "next-pin": { - "name": "Next.js listener source", - "basis": "v16.3.6", - "sources": { - "s11ffc231f0c3": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246", - "s3b0feef8ec0f": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/bin/next.ts#L436-L458", - "s0ef9150d51e4": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L44", - "sefdefa3425c6": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L84-L87", - "s66bd2ed9c346": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L187-L196", - "s2b4479b3c1d2": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L309" - } - }, - "node": { - "name": "Node.js listener source", - "basis": "v22.22.1", - "sources": { - "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" - } - }, - "commander": { - "name": "Commander", - "basis": "12.1.0", - "sources": { - "s408eb603ae2e": "https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205" - } - }, - "authjs": { - "name": "Auth.js", - "basis": "unknown", - "sources": { - "sb4ac5f50ec66": "https://authjs.dev/getting-started/deployment", - "s42365460cb4c": "https://authjs.dev/getting-started/installation", - "s46bb80456d4f": "https://authjs.dev/getting-started/session-management/protecting" - } - }, - "better": { - "name": "Better Auth", - "basis": "unknown", - "sources": { - "s981751bcb554": "https://better-auth.com/docs/installation", - "s2ac2e0926f25": "https://better-auth.com/docs/integrations/next", - "sdd3d825cc3a4": "https://better-auth.com/docs/introduction", - "s8677a74455a1": "https://better-auth.com/docs/plugins/2fa", - "s4b3a514c43d9": "https://better-auth.com/docs/reference/options" - } - }, - "vercel": { - "name": "Vercel documentation", - "basis": "unknown", - "sources": { - "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", - "se6089d5c4e96": "https://vercel.com/docs/deployment-protection" - } - }, - "curl": { - "name": "curl", - "basis": "unknown", - "sources": { - "s5345a985007a": "https://curl.se/docs/manpage.html#-H" - } - }, - "grep": { - "name": "GNU grep", - "basis": "unknown", - "sources": { - "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", - "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", - "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" - } - } - }, - "claims": { - "entrypoints": {"text": "Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sc2a39ce10fe1", "next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "start-default": {"text": "v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined.", "components": ["next-pin", "node", "commander"], "sources": ["next-pin:s3b0feef8ec0f", "next-pin:s0ef9150d51e4", "next-pin:sefdefa3425c6", "next-pin:s66bd2ed9c346", "next-pin:s2b4479b3c1d2", "node:sfb3e306c102d", "commander:s408eb603ae2e", "next-pin:s11ffc231f0c3"], "status": "REASONED"}, - "private-tls": {"text": "Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED"}, - "dal": {"text": "Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "layouts": {"text": "Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "proxy": {"text": "Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sa45045bebccb"], "status": "REASONED"}, - "role": {"text": "Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "session-secret": {"text": "Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s964651d4eed4"], "status": "REASONED"}, - "session-jwt": {"text": "jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED"}, - "session-cookie": {"text": "Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session.", "components": ["next"], "sources": ["next:sfbc4364fb5cc", "next:sa45045bebccb"], "status": "REASONED"}, - "public-env": {"text": "NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files.", "components": ["next"], "sources": ["next:s964651d4eed4", "next:s23a087193901"], "status": "REASONED"}, - "action-key": {"text": "Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection.", "components": ["next"], "sources": ["next:s353c30dff99f", "next:s23a087193901"], "status": "REASONED"}, - "action-origin": {"text": "Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins.", "components": ["next"], "sources": ["next:s23a087193901", "next:s353c30dff99f"], "status": "REASONED"}, - "authjs-secret": {"text": "AUTH_SECRET is mandatory; npx auth secret writes .env.local.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, - "authjs-providers": {"text": "Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, - "authjs-trust": {"text": "Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66"], "status": "REASONED"}, - "authjs-route": {"text": "auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, - "authjs-proxy": {"text": "auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, - "better-secret": {"text": "BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference.", "components": ["better"], "sources": ["better:s981751bcb554", "better:s4b3a514c43d9"], "status": "REASONED"}, - "better-origins": {"text": "trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, - "better-password": {"text": "Better Auth emailAndPassword defaults disabled; enable explicitly when used.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, - "better-next": {"text": "nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, - "better-session": {"text": "Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, - "better-mfa": {"text": "twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks.", "components": ["better"], "sources": ["better:s8677a74455a1"], "status": "REASONED"}, - "vercel-methods": {"text": "Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, - "vercel-scope": {"text": "Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby.", "components": ["vercel"], "sources": ["vercel:s7836e043434a", "vercel:se6089d5c4e96"], "status": "REASONED"}, - "verify-bind": {"text": "ss inventories loopback 3000; it does not prove firewall/platform ingress isolation.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED", "verify": [1]}, - "verify-anonymous": {"text": "Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, - "verify-authorized": {"text": "Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, - "verify-cookie-input": {"text": "Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes.", "components": ["curl"], "sources": ["curl:s5345a985007a"], "status": "REASONED", "verify": [1]}, - "verify-secret": {"text": "Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape.", "components": ["next", "grep"], "sources": ["next:s964651d4eed4", "grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae"], "status": "REASONED", "verify": [1]}, - "verify-action": {"text": "Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED", "verify": [1]} - } -} ---- # Next.js: authentication that actually gates Route Handlers, Server Actions, and data - -**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 Next.js listener source v16.3.6, Node.js listener source v22.22.1, and Commander 12.1.0; 6 further sources carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 32 claims, all reasoned from cited sources. -Components (recorded basis): - -- next: Next.js documentation (unknown) -- next-pin: Next.js listener source (v16.3.6) -- node: Node.js listener source (v22.22.1) -- commander: Commander (12.1.0) -- authjs: Auth.js (unknown) -- better: Better Auth (unknown) -- vercel: Vercel documentation (unknown) -- curl: curl (unknown) -- grep: GNU grep (unknown) - -Claims (full source mapping in the front matter): - -- entrypoints (REASONED; next): Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login. -- start-default (REASONED; next-pin, node, commander): v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined. -- private-tls (REASONED; next): Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform. -- dal (REASONED; next): Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields. -- layouts (REASONED; next): Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions. -- proxy (REASONED; next): Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes. -- role (REASONED; next): Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses. -- session-secret (REASONED; next): Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_. -- session-jwt (REASONED; next): jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions. -- session-cookie (REASONED; next): Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session. -- public-env (REASONED; next): NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files. -- action-key (REASONED; next): Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection. -- action-origin (REASONED; next): Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins. -- authjs-secret (REASONED; authjs): AUTH_SECRET is mandatory; npx auth secret writes .env.local. -- authjs-providers (REASONED; authjs): Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks. -- authjs-trust (REASONED; authjs): Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically. -- authjs-route (REASONED; authjs): auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data. -- authjs-proxy (REASONED; authjs): auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider. -- better-secret (REASONED; better): BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference. -- better-origins (REASONED; better): trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks. -- better-password (REASONED; better): Better Auth emailAndPassword defaults disabled; enable explicitly when used. -- better-next (REASONED; better): nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes. -- better-session (REASONED; better): Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing. -- better-mfa (REASONED; better): twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks. -- vercel-methods (REASONED; vercel): Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication. -- vercel-scope (REASONED; vercel): Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby. -- verify-bind (REASONED; next): ss inventories loopback 3000; it does not prove firewall/platform ingress isolation. -- verify-anonymous (REASONED; next): Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies. -- verify-authorized (REASONED; next): Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success. -- verify-cookie-input (REASONED; curl): Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes. -- verify-secret (REASONED; next, grep): Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape. -- verify-action (REASONED; next): Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found. - +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 Next.js app has many entry points: pages, layouts, Route Handlers (`app/**/route.ts`), and every exported Server Action, which the Next.js docs describe as reachable by a direct POST whether or not your UI calls it. A check that lives only in a layout or in `proxy.ts` (the file Next.js 16 renamed from `middleware.ts`) leaves open the entry points it does not cover (a layout guards no Route Handler or Server Action; a `proxy.ts` matcher guards only the paths it matches), and authorization still belongs next to the data. Next.js supplies cookies and sessions as primitives, not a login system; the login is yours or a library's. @@ -422,3 +243,208 @@ curl -q -sS --noproxy '*' -o dash-nocookie.txt -w 'http=%{http_code} loc=%{redi - Commander 12.1.0 (the version Next.js v16.3.6 pins): an option that takes a value is `undefined` unless specified on the command line: https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205 - Next.js v16.3.6 pins `commander` at `12.1.0` (pinned tag v16.3.6): https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246 - curl `--header @-` (header lines read from stdin): https://curl.se/docs/manpage.html#-H ; GNU grep `-f -` ("When file is '-', read patterns from standard input"), `-l` ("print the name of each input file from which output would normally have been printed") and its exit status (0 if a line is selected, 1 if none, 2 on error): https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html , https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html and https://www.gnu.org/software/grep/manual/html_node/Exit-Status.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/nextjs.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): + +- next-pin: Next.js listener source (v16.3.6) +- next: Next.js documentation (unknown) +- node: Node.js listener source (v22.22.1) +- commander: Commander (12.1.0) +- authjs: Auth.js (unknown) +- better: Better Auth (unknown) +- vercel: Vercel documentation (unknown) +- curl: curl (unknown) +- grep: GNU grep (unknown) + +Claims (full source mapping in the JSON block below): + +- entrypoints (REASONED; next): Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login. +- start-default (REASONED; next-pin, node, commander): v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined. +- private-tls (REASONED; next): Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform. +- dal (REASONED; next): Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields. +- layouts (REASONED; next): Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions. +- proxy (REASONED; next): Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes. +- role (REASONED; next): Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses. +- session-secret (REASONED; next): Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_. +- session-jwt (REASONED; next): jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions. +- session-cookie (REASONED; next): Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session. +- public-env (REASONED; next): NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files. +- action-key (REASONED; next): Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection. +- action-origin (REASONED; next): Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins. +- authjs-secret (REASONED; authjs): AUTH_SECRET is mandatory; npx auth secret writes .env.local. +- authjs-providers (REASONED; authjs): Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks. +- authjs-trust (REASONED; authjs): Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically. +- authjs-route (REASONED; authjs): auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data. +- authjs-proxy (REASONED; authjs): auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider. +- better-secret (REASONED; better): BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference. +- better-origins (REASONED; better): trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks. +- better-password (REASONED; better): Better Auth emailAndPassword defaults disabled; enable explicitly when used. +- better-next (REASONED; better): nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes. +- better-session (REASONED; better): Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing. +- better-mfa (REASONED; better): twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks. +- vercel-methods (REASONED; vercel): Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication. +- vercel-scope (REASONED; vercel): Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby. +- verify-bind (REASONED; next): ss inventories loopback 3000; it does not prove firewall/platform ingress isolation. +- verify-anonymous (REASONED; next): Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies. +- verify-authorized (REASONED; next): Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success. +- verify-cookie-input (REASONED; curl): Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes. +- verify-secret (REASONED; next, grep): Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape. +- verify-action (REASONED; next): Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "3bdad54327114894a6f45d5d5adb334b0b4fbc9bf784629b0385e6e2c4063461", + "components": { + "next-pin": { + "name": "Next.js listener source", + "basis": "v16.3.6", + "sources": { + "s11ffc231f0c3": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246", + "s3b0feef8ec0f": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/bin/next.ts#L436-L458", + "s0ef9150d51e4": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L44", + "sefdefa3425c6": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L84-L87", + "s66bd2ed9c346": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L187-L196", + "s2b4479b3c1d2": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L309" + } + }, + "next": { + "name": "Next.js documentation", + "basis": "unknown", + "sources": { + "s066f8a176a52": "https://nextjs.org/docs/app/api-reference/cli/next", + "scd5749a71a79": "https://nextjs.org/docs/app/api-reference/file-conventions/proxy", + "sc2a39ce10fe1": "https://nextjs.org/docs/app/api-reference/file-conventions/route", + "sfbc4364fb5cc": "https://nextjs.org/docs/app/api-reference/functions/cookies", + "sa45045bebccb": "https://nextjs.org/docs/app/guides/authentication", + "s23a087193901": "https://nextjs.org/docs/app/guides/data-security", + "s964651d4eed4": "https://nextjs.org/docs/app/guides/environment-variables", + "s353c30dff99f": "https://nextjs.org/docs/app/guides/self-hosting" + } + }, + "node": { + "name": "Node.js listener source", + "basis": "v22.22.1", + "sources": { + "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" + } + }, + "commander": { + "name": "Commander", + "basis": "12.1.0", + "sources": { + "s408eb603ae2e": "https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205" + } + }, + "authjs": { + "name": "Auth.js", + "basis": "unknown", + "sources": { + "sb4ac5f50ec66": "https://authjs.dev/getting-started/deployment", + "s42365460cb4c": "https://authjs.dev/getting-started/installation", + "s46bb80456d4f": "https://authjs.dev/getting-started/session-management/protecting" + } + }, + "better": { + "name": "Better Auth", + "basis": "unknown", + "sources": { + "s981751bcb554": "https://better-auth.com/docs/installation", + "s2ac2e0926f25": "https://better-auth.com/docs/integrations/next", + "sdd3d825cc3a4": "https://better-auth.com/docs/introduction", + "s8677a74455a1": "https://better-auth.com/docs/plugins/2fa", + "s4b3a514c43d9": "https://better-auth.com/docs/reference/options" + } + }, + "vercel": { + "name": "Vercel documentation", + "basis": "unknown", + "sources": { + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection" + } + }, + "curl": { + "name": "curl", + "basis": "unknown", + "sources": { + "s5345a985007a": "https://curl.se/docs/manpage.html#-H" + } + }, + "grep": { + "name": "GNU grep", + "basis": "unknown", + "sources": { + "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", + "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", + "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" + } + } + }, + "claims": { + "entrypoints": {"text": "Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sc2a39ce10fe1", "next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "start-default": {"text": "v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined.", "components": ["next-pin", "node", "commander"], "sources": ["next-pin:s3b0feef8ec0f", "next-pin:s0ef9150d51e4", "next-pin:sefdefa3425c6", "next-pin:s66bd2ed9c346", "next-pin:s2b4479b3c1d2", "node:sfb3e306c102d", "commander:s408eb603ae2e", "next-pin:s11ffc231f0c3"], "status": "REASONED"}, + "private-tls": {"text": "Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED"}, + "dal": {"text": "Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "layouts": {"text": "Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "proxy": {"text": "Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sa45045bebccb"], "status": "REASONED"}, + "role": {"text": "Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "session-secret": {"text": "Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s964651d4eed4"], "status": "REASONED"}, + "session-jwt": {"text": "jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED"}, + "session-cookie": {"text": "Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session.", "components": ["next"], "sources": ["next:sfbc4364fb5cc", "next:sa45045bebccb"], "status": "REASONED"}, + "public-env": {"text": "NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files.", "components": ["next"], "sources": ["next:s964651d4eed4", "next:s23a087193901"], "status": "REASONED"}, + "action-key": {"text": "Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection.", "components": ["next"], "sources": ["next:s353c30dff99f", "next:s23a087193901"], "status": "REASONED"}, + "action-origin": {"text": "Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins.", "components": ["next"], "sources": ["next:s23a087193901", "next:s353c30dff99f"], "status": "REASONED"}, + "authjs-secret": {"text": "AUTH_SECRET is mandatory; npx auth secret writes .env.local.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, + "authjs-providers": {"text": "Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, + "authjs-trust": {"text": "Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66"], "status": "REASONED"}, + "authjs-route": {"text": "auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, + "authjs-proxy": {"text": "auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, + "better-secret": {"text": "BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference.", "components": ["better"], "sources": ["better:s981751bcb554", "better:s4b3a514c43d9"], "status": "REASONED"}, + "better-origins": {"text": "trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, + "better-password": {"text": "Better Auth emailAndPassword defaults disabled; enable explicitly when used.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, + "better-next": {"text": "nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, + "better-session": {"text": "Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, + "better-mfa": {"text": "twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks.", "components": ["better"], "sources": ["better:s8677a74455a1"], "status": "REASONED"}, + "vercel-methods": {"text": "Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "vercel-scope": {"text": "Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby.", "components": ["vercel"], "sources": ["vercel:s7836e043434a", "vercel:se6089d5c4e96"], "status": "REASONED"}, + "verify-bind": {"text": "ss inventories loopback 3000; it does not prove firewall/platform ingress isolation.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED", "verify": [1]}, + "verify-anonymous": {"text": "Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, + "verify-authorized": {"text": "Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, + "verify-cookie-input": {"text": "Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes.", "components": ["curl"], "sources": ["curl:s5345a985007a"], "status": "REASONED", "verify": [1]}, + "verify-secret": {"text": "Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape.", "components": ["next", "grep"], "sources": ["next:s964651d4eed4", "grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae"], "status": "REASONED", "verify": [1]}, + "verify-action": {"text": "Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/paas.md b/paas.md index 2f707d2ab..a3f7a14bc 100644 --- a/paas.md +++ b/paas.md @@ -1,214 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "2db22068c994d124ee04bbb96560a48925d4f295f118e343bc0e18ff0d9155b5", - "components": { - "render": { - "name": "Render documentation", - "basis": "unknown", - "sources": { - "s213783ba6209": "https://render.com/docs", - "se240afec8d2d": "https://render.com/docs/postgresql-creating-connecting", - "s1820ac471b6c": "https://render.com/docs/web-services", - "s5b8400c2a2ba": "https://render.com/docs/tls", - "s22963948fc90": "https://render.com/docs/custom-domains" - } - }, - "fly": { - "name": "Fly.io documentation", - "basis": "unknown", - "sources": { - "s1b202c527f2e": "https://fly.io/docs" - } - }, - "vercel": { - "name": "Vercel documentation", - "basis": "unknown", - "sources": { - "s2b470a6bc853": "https://vercel.com/docs", - "se6089d5c4e96": "https://vercel.com/docs/deployment-protection", - "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", - "s3e7832fd3031": "https://vercel.com/docs/cdn-security/encryption", - "se314aa4b7f1e": "https://vercel.com/docs/domains/working-with-ssl", - "s22498401f5d0": "https://vercel.com/docs/functions/runtimes", - "s845513031876": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection", - "sc1588029e14e": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation" - } - }, - "spaces": { - "name": "Hugging Face Spaces documentation", - "basis": "unknown", - "sources": { - "sda0c810c053d": "https://huggingface.co/docs/hub/spaces-overview", - "sd5ec2b2772e0": "https://huggingface.co/docs/hub/spaces-config-reference" - } - }, - "nextjs": { - "name": "Next.js environment variables", - "basis": "15", - "sources": { - "s22e4b70dab8b": "https://nextjs.org/docs/15/app/guides/environment-variables" - } - }, - "express": { - "name": "Express request protocol", - "basis": "5.x", - "sources": { - "s5bc62e66824b": "https://expressjs.com/en/5x/api/request/#req.protocol" - } - }, - "django": { - "name": "Django proxy security setting", - "basis": "6.1", - "sources": { - "sb5e26f65fd91": "https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header" - } - }, - "mysql-client": { - "name": "MySQL client connection options", - "basis": "8.4", - "sources": { - "s823c0bfaaafa": "https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode" - } - }, - "spaces-pinned": { - "name": "Hugging Face Hub documentation", - "basis": "6fb324369143cb76a723c88a592010514ac1095a", - "sources": { - "se2fb4e2a5c02": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45", - "s9d332f0940e2": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118", - "s969a28a4c44e": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27", - "s5d393b019bad": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14", - "sd1ae2b703a70": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78" - } - }, - "heroku": { - "name": "Heroku documentation", - "basis": "unknown", - "sources": { - "sa86eb06bbd4a": "https://devcenter.heroku.com/articles/automated-certificate-management", - "s0c5bf300cef4": "https://devcenter.heroku.com/articles/http-routing", - "sfbcf716168be": "https://devcenter.heroku.com/articles/custom-domains", - "s370d4beec29a": "https://devcenter.heroku.com/articles/runtime-principles", - "s211f5fe76042": "https://devcenter.heroku.com/articles/generations" - } - }, - "fly-pinned": { - "name": "Fly.io pinned documentation", - "basis": "2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9", - "sources": { - "s254c8c8a5edd": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97", - "s32999aa4c58c": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176", - "sf4105f79b22b": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44", - "sea5560c14d13": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87", - "sb935e8478a30": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102", - "s62fec91543a1": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42", - "s61c1e50d2c11": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92", - "s6e9a7292860d": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7", - "sa63b828c93df": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350", - "s4324a02dbc42": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229", - "scf2e6f3bce02": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123" - } - }, - "railway": { - "name": "Railway documentation", - "basis": "72354493198302677fb6be6c692dd75c64cae2c6", - "sources": { - "s93da6ccbfe58": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122", - "s6d4dc8fb7784": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247", - "s2bc94f13536b": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150", - "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146", - "sbf08758c106a": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17", - "s844d338bd121": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38" - } - }, - "nextjs-pinned": { - "name": "Next.js pinned documentation", - "basis": "v16.4.2", - "sources": { - "s0e3d2cd2248e": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164", - "sf7cfdc553311": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" - } - } - }, - "claims": { - "managed-tls": {"text": "TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "render:s5b8400c2a2ba", "render:s22963948fc90", "vercel:s3e7832fd3031", "vercel:se314aa4b7f1e", "heroku:sa86eb06bbd4a", "heroku:s0c5bf300cef4", "heroku:sfbcf716168be", "fly-pinned:s254c8c8a5edd", "fly-pinned:s32999aa4c58c", "fly-pinned:sf4105f79b22b", "fly-pinned:sea5560c14d13", "fly-pinned:sb935e8478a30", "railway:s93da6ccbfe58", "railway:s6d4dc8fb7784", "railway:s2bc94f13536b"], "status": "REASONED"}, - "application-auth": {"text": "Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, - "vercel-login": {"text": "Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s7836e043434a"], "status": "REASONED"}, - "vercel-password": {"text": "At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, - "secret-store": {"text": "Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles.", "components": ["render", "fly", "vercel"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853"], "status": "REASONED"}, - "public-env": {"text": "Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_.", "components": ["vercel", "nextjs"], "sources": ["vercel:s2b470a6bc853", "nextjs:s22e4b70dab8b"], "status": "REASONED"}, - "https-redirect": {"text": "Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s5b8400c2a2ba", "vercel:s3e7832fd3031", "heroku:s0c5bf300cef4", "express:s5bc62e66824b", "django:sb5e26f65fd91", "fly-pinned:s6e9a7292860d", "fly-pinned:sa63b828c93df", "fly-pinned:s4324a02dbc42", "fly-pinned:scf2e6f3bce02", "railway:sbf08758c106a", "railway:s844d338bd121"], "status": "REASONED"}, - "proxy-awareness": {"text": "Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform.", "components": ["render", "fly", "vercel", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "express:s5bc62e66824b", "django:sb5e26f65fd91"], "status": "REASONED"}, - "listener-port": {"text": "Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "vercel:s22498401f5d0", "heroku:s370d4beec29a", "heroku:s211f5fe76042", "fly-pinned:s62fec91543a1", "fly-pinned:s61c1e50d2c11", "railway:sac1383056b65"], "status": "REASONED"}, - "database-identity": {"text": "Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY.", "components": ["render", "mysql-client"], "sources": ["render:se240afec8d2d", "mysql-client:s823c0bfaaafa"], "status": "REASONED"}, - "render-internal-tls": {"text": "Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, - "database-network": {"text": "Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, - "spaces-creation": {"text": "This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:se2fb4e2a5c02", "spaces-pinned:s9d332f0940e2", "spaces-pinned:s969a28a4c44e"], "status": "REASONED"}, - "spaces-public": {"text": "Public Spaces expose both source and running app.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-protected": {"text": "Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-private": {"text": "Private Spaces restrict both source and running app to the owner and collaborators.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-secrets": {"text": "Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d", "spaces:sd5ec2b2772e0"], "status": "REASONED"}, - "spaces-static": {"text": "Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-dev": {"text": "At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:s5d393b019bad", "spaces-pinned:sd1ae2b703a70"], "status": "REASONED"}, - "verify-redirect": {"text": "This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness.", "components": ["vercel", "render", "fly-pinned", "railway", "heroku"], "sources": ["vercel:s2b470a6bc853", "vercel:s3e7832fd3031", "render:s5b8400c2a2ba", "fly-pinned:s4324a02dbc42", "railway:sbf08758c106a", "railway:s844d338bd121", "heroku:s0c5bf300cef4"], "status": "REASONED", "verify": [1]}, - "verify-app-auth": {"text": "Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s845513031876", "vercel:sc1588029e14e"], "status": "REASONED", "verify": [1]}, - "verify-secrets": {"text": "Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan.", "components": ["render", "fly", "vercel", "nextjs-pinned"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "nextjs-pinned:s0e3d2cd2248e", "nextjs-pinned:sf7cfdc553311"], "status": "REASONED", "verify": [1]} - } -} ---- # PaaS platforms: what the platform does, what stays yours - -**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): - -- render: Render documentation (unknown) -- fly: Fly.io documentation (unknown) -- vercel: Vercel documentation (unknown) -- spaces: Hugging Face Spaces documentation (unknown) -- nextjs: Next.js environment variables (15) -- express: Express request protocol (5.x) -- django: Django proxy security setting (6.1) -- mysql-client: MySQL client connection options (8.4) -- spaces-pinned: Hugging Face Hub documentation (6fb324369143cb76a723c88a592010514ac1095a) -- heroku: Heroku documentation (unknown) -- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) -- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) -- nextjs-pinned: Next.js pinned documentation (v16.4.2) +Written against Render documentation (no recorded version), Next.js pinned documentation v16.4.2, Next.js environment variables 15, Express request protocol 5.x, Django proxy security setting 6.1, and MySQL client connection options 8.4; 3 further sources are pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 22 claims, all reasoned from cited sources. -Claims (full source mapping in the front matter): - -- managed-tls (REASONED; render, fly, vercel, heroku, fly-pinned, railway): TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate. -- application-auth (REASONED; vercel): Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls. -- vercel-login (REASONED; vercel): Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby. -- vercel-password (REASONED; vercel): At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise. -- secret-store (REASONED; render, fly, vercel): Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles. -- public-env (REASONED; vercel, nextjs): Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_. -- https-redirect (REASONED; render, fly, vercel, heroku, fly-pinned, railway, express, django): Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization. -- proxy-awareness (REASONED; render, fly, vercel, express, django): Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform. -- listener-port (REASONED; render, fly, vercel, heroku, fly-pinned, railway): Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners. -- database-identity (REASONED; render, mysql-client): Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY. -- render-internal-tls (REASONED; render): Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification. -- database-network (REASONED; render): Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store. -- spaces-creation (REASONED; spaces, spaces-pinned): This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only. -- spaces-public (REASONED; spaces): Public Spaces expose both source and running app. -- spaces-protected (REASONED; spaces): Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations. -- spaces-private (REASONED; spaces): Private Spaces restrict both source and running app to the owner and collaborators. -- spaces-secrets (REASONED; spaces): Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable. -- spaces-static (REASONED; spaces): Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service. -- spaces-dev (REASONED; spaces, spaces-pinned): At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path. -- verify-redirect (REASONED; vercel, render, fly-pinned, railway, heroku): This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness. -- verify-app-auth (REASONED; vercel): Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement. -- verify-secrets (REASONED; render, fly, vercel, nextjs-pinned): Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan. - +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 responsibilities depend on the platform and deployment mode: @@ -332,3 +126,235 @@ curl -q -g -s --noproxy '*' -o /dev/null -w 'http=%{http_code}\n' https://app.ex - Fly HTTP handler forwarded-protocol header (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123 - Railway public-networking headers and request handling (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17 - Railway GET redirect status and POST conversion (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38 + + + +## 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/paas.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): + +- render: Render documentation (unknown) +- fly: Fly.io documentation (unknown) +- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) +- vercel: Vercel documentation (unknown) +- spaces: Hugging Face Spaces documentation (unknown) +- spaces-pinned: Hugging Face Hub documentation (6fb324369143cb76a723c88a592010514ac1095a) +- heroku: Heroku documentation (unknown) +- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) +- nextjs-pinned: Next.js pinned documentation (v16.4.2) +- nextjs: Next.js environment variables (15) +- express: Express request protocol (5.x) +- django: Django proxy security setting (6.1) +- mysql-client: MySQL client connection options (8.4) + +Claims (full source mapping in the JSON block below): + +- managed-tls (REASONED; render, fly, vercel, heroku, fly-pinned, railway): TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate. +- application-auth (REASONED; vercel): Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls. +- vercel-login (REASONED; vercel): Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby. +- vercel-password (REASONED; vercel): At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise. +- secret-store (REASONED; render, fly, vercel): Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles. +- public-env (REASONED; vercel, nextjs): Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_. +- https-redirect (REASONED; render, fly, vercel, heroku, fly-pinned, railway, express, django): Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization. +- proxy-awareness (REASONED; render, fly, vercel, express, django): Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform. +- listener-port (REASONED; render, fly, vercel, heroku, fly-pinned, railway): Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners. +- database-identity (REASONED; render, mysql-client): Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY. +- render-internal-tls (REASONED; render): Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification. +- database-network (REASONED; render): Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store. +- spaces-creation (REASONED; spaces, spaces-pinned): This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only. +- spaces-public (REASONED; spaces): Public Spaces expose both source and running app. +- spaces-protected (REASONED; spaces): Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations. +- spaces-private (REASONED; spaces): Private Spaces restrict both source and running app to the owner and collaborators. +- spaces-secrets (REASONED; spaces): Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable. +- spaces-static (REASONED; spaces): Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service. +- spaces-dev (REASONED; spaces, spaces-pinned): At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path. +- verify-redirect (REASONED; vercel, render, fly-pinned, railway, heroku): This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness. +- verify-app-auth (REASONED; vercel): Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement. +- verify-secrets (REASONED; render, fly, vercel, nextjs-pinned): Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "2db22068c994d124ee04bbb96560a48925d4f295f118e343bc0e18ff0d9155b5", + "components": { + "render": { + "name": "Render documentation", + "basis": "unknown", + "sources": { + "s213783ba6209": "https://render.com/docs", + "se240afec8d2d": "https://render.com/docs/postgresql-creating-connecting", + "s1820ac471b6c": "https://render.com/docs/web-services", + "s5b8400c2a2ba": "https://render.com/docs/tls", + "s22963948fc90": "https://render.com/docs/custom-domains" + } + }, + "fly": { + "name": "Fly.io documentation", + "basis": "unknown", + "sources": { + "s1b202c527f2e": "https://fly.io/docs" + } + }, + "fly-pinned": { + "name": "Fly.io pinned documentation", + "basis": "2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9", + "sources": { + "s254c8c8a5edd": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97", + "s32999aa4c58c": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176", + "sf4105f79b22b": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44", + "sea5560c14d13": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87", + "sb935e8478a30": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102", + "s62fec91543a1": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42", + "s61c1e50d2c11": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92", + "s6e9a7292860d": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7", + "sa63b828c93df": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350", + "s4324a02dbc42": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229", + "scf2e6f3bce02": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123" + } + }, + "vercel": { + "name": "Vercel documentation", + "basis": "unknown", + "sources": { + "s2b470a6bc853": "https://vercel.com/docs", + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection", + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", + "s3e7832fd3031": "https://vercel.com/docs/cdn-security/encryption", + "se314aa4b7f1e": "https://vercel.com/docs/domains/working-with-ssl", + "s22498401f5d0": "https://vercel.com/docs/functions/runtimes", + "s845513031876": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection", + "sc1588029e14e": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation" + } + }, + "spaces": { + "name": "Hugging Face Spaces documentation", + "basis": "unknown", + "sources": { + "sda0c810c053d": "https://huggingface.co/docs/hub/spaces-overview", + "sd5ec2b2772e0": "https://huggingface.co/docs/hub/spaces-config-reference" + } + }, + "spaces-pinned": { + "name": "Hugging Face Hub documentation", + "basis": "6fb324369143cb76a723c88a592010514ac1095a", + "sources": { + "se2fb4e2a5c02": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45", + "s9d332f0940e2": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118", + "s969a28a4c44e": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27", + "s5d393b019bad": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14", + "sd1ae2b703a70": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78" + } + }, + "heroku": { + "name": "Heroku documentation", + "basis": "unknown", + "sources": { + "sa86eb06bbd4a": "https://devcenter.heroku.com/articles/automated-certificate-management", + "s0c5bf300cef4": "https://devcenter.heroku.com/articles/http-routing", + "sfbcf716168be": "https://devcenter.heroku.com/articles/custom-domains", + "s370d4beec29a": "https://devcenter.heroku.com/articles/runtime-principles", + "s211f5fe76042": "https://devcenter.heroku.com/articles/generations" + } + }, + "railway": { + "name": "Railway documentation", + "basis": "72354493198302677fb6be6c692dd75c64cae2c6", + "sources": { + "s93da6ccbfe58": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122", + "s6d4dc8fb7784": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247", + "s2bc94f13536b": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150", + "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146", + "sbf08758c106a": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17", + "s844d338bd121": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38" + } + }, + "nextjs-pinned": { + "name": "Next.js pinned documentation", + "basis": "v16.4.2", + "sources": { + "s0e3d2cd2248e": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164", + "sf7cfdc553311": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" + } + }, + "nextjs": { + "name": "Next.js environment variables", + "basis": "15", + "sources": { + "s22e4b70dab8b": "https://nextjs.org/docs/15/app/guides/environment-variables" + } + }, + "express": { + "name": "Express request protocol", + "basis": "5.x", + "sources": { + "s5bc62e66824b": "https://expressjs.com/en/5x/api/request/#req.protocol" + } + }, + "django": { + "name": "Django proxy security setting", + "basis": "6.1", + "sources": { + "sb5e26f65fd91": "https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header" + } + }, + "mysql-client": { + "name": "MySQL client connection options", + "basis": "8.4", + "sources": { + "s823c0bfaaafa": "https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode" + } + } + }, + "claims": { + "managed-tls": {"text": "TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "render:s5b8400c2a2ba", "render:s22963948fc90", "vercel:s3e7832fd3031", "vercel:se314aa4b7f1e", "heroku:sa86eb06bbd4a", "heroku:s0c5bf300cef4", "heroku:sfbcf716168be", "fly-pinned:s254c8c8a5edd", "fly-pinned:s32999aa4c58c", "fly-pinned:sf4105f79b22b", "fly-pinned:sea5560c14d13", "fly-pinned:sb935e8478a30", "railway:s93da6ccbfe58", "railway:s6d4dc8fb7784", "railway:s2bc94f13536b"], "status": "REASONED"}, + "application-auth": {"text": "Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "vercel-login": {"text": "Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s7836e043434a"], "status": "REASONED"}, + "vercel-password": {"text": "At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "secret-store": {"text": "Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles.", "components": ["render", "fly", "vercel"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853"], "status": "REASONED"}, + "public-env": {"text": "Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_.", "components": ["vercel", "nextjs"], "sources": ["vercel:s2b470a6bc853", "nextjs:s22e4b70dab8b"], "status": "REASONED"}, + "https-redirect": {"text": "Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s5b8400c2a2ba", "vercel:s3e7832fd3031", "heroku:s0c5bf300cef4", "express:s5bc62e66824b", "django:sb5e26f65fd91", "fly-pinned:s6e9a7292860d", "fly-pinned:sa63b828c93df", "fly-pinned:s4324a02dbc42", "fly-pinned:scf2e6f3bce02", "railway:sbf08758c106a", "railway:s844d338bd121"], "status": "REASONED"}, + "proxy-awareness": {"text": "Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform.", "components": ["render", "fly", "vercel", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "express:s5bc62e66824b", "django:sb5e26f65fd91"], "status": "REASONED"}, + "listener-port": {"text": "Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "vercel:s22498401f5d0", "heroku:s370d4beec29a", "heroku:s211f5fe76042", "fly-pinned:s62fec91543a1", "fly-pinned:s61c1e50d2c11", "railway:sac1383056b65"], "status": "REASONED"}, + "database-identity": {"text": "Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY.", "components": ["render", "mysql-client"], "sources": ["render:se240afec8d2d", "mysql-client:s823c0bfaaafa"], "status": "REASONED"}, + "render-internal-tls": {"text": "Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, + "database-network": {"text": "Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, + "spaces-creation": {"text": "This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:se2fb4e2a5c02", "spaces-pinned:s9d332f0940e2", "spaces-pinned:s969a28a4c44e"], "status": "REASONED"}, + "spaces-public": {"text": "Public Spaces expose both source and running app.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-protected": {"text": "Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-private": {"text": "Private Spaces restrict both source and running app to the owner and collaborators.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-secrets": {"text": "Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d", "spaces:sd5ec2b2772e0"], "status": "REASONED"}, + "spaces-static": {"text": "Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-dev": {"text": "At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:s5d393b019bad", "spaces-pinned:sd1ae2b703a70"], "status": "REASONED"}, + "verify-redirect": {"text": "This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness.", "components": ["vercel", "render", "fly-pinned", "railway", "heroku"], "sources": ["vercel:s2b470a6bc853", "vercel:s3e7832fd3031", "render:s5b8400c2a2ba", "fly-pinned:s4324a02dbc42", "railway:sbf08758c106a", "railway:s844d338bd121", "heroku:s0c5bf300cef4"], "status": "REASONED", "verify": [1]}, + "verify-app-auth": {"text": "Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s845513031876", "vercel:sc1588029e14e"], "status": "REASONED", "verify": [1]}, + "verify-secrets": {"text": "Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan.", "components": ["render", "fly", "vercel", "nextjs-pinned"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "nextjs-pinned:s0e3d2cd2248e", "nextjs-pinned:sf7cfdc553311"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/php.md b/php.md index ed3eab7a3..a83fd5cd8 100644 --- a/php.md +++ b/php.md @@ -1,250 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "b65232e7b90f8edc69a6b0b2639a04b3f7b6b4590b7015c4d1d84cd70d5f9aae", - "components": { - "php": { - "name": "PHP documentation", - "basis": "unknown", - "sources": { - "s09fdbeba0867": "https://www.php.net/manual/en/curl.constants.php", - "s5840a5a0a848": "https://www.php.net/manual/en/features.commandline.options.php", - "s3cbc65590d52": "https://www.php.net/manual/en/features.file-upload.common-pitfalls.php", - "s6f3e55d536e7": "https://www.php.net/manual/en/filesystem.configuration.php", - "s50f85ff4fbd1": "https://www.php.net/manual/en/function.password-hash.php", - "s625cd1d40655": "https://www.php.net/manual/en/function.session-regenerate-id.php", - "s2f1c806d037c": "https://www.php.net/manual/en/function.trigger-error.php", - "s7b2c2c19835d": "https://www.php.net/manual/en/security.hiding.php", - "s8664dc56a028": "https://www.php.net/manual/en/session.security.ini.php" - } - }, - "curl": { - "name": "curl manual", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "php8": { - "name": "PHP native configuration", - "basis": "8.x", - "sources": { - "s89c799077b82": "https://www.php.net/manual/en/errorfunc.configuration.php", - "s5a3f7bde8338": "https://www.php.net/manual/en/info.configuration.php", - "sc9e417e2ad6f": "https://www.php.net/manual/en/ini.core.php", - "s2e692b7c86a6": "https://www.php.net/manual/en/install.fpm.configuration.php", - "s000e155a7b25": "https://www.php.net/manual/en/session.configuration.php" - } - }, - "template84": { - "name": "PHP production template", - "basis": "8.4", - "sources": { - "sd72e3d69551f": "https://raw.githubusercontent.com/php/php-src/d313ad6098430f4e61f0121a9e7ab392d195e4e4/php.ini-production" - } - }, - "template83": { - "name": "PHP production template", - "basis": "8.3", - "sources": { - "sea3d3a4ea486": "https://raw.githubusercontent.com/php/php-src/b94f9f68a610e0fca5f2fea5bfa7c7d6d3d5a847/php.ini-production" - } - }, - "laravel": { - "name": "Laravel", - "basis": "12.x", - "sources": { - "sf0448f3d4665": "https://api.laravel.com/docs/12.x/Illuminate/Routing/UrlGenerator.html", - "s98db3429151c": "https://github.com/laravel/laravel/blob/f6b2e79bdbfc5bf4a37ad16466cc06ad79cc9e8f/config/session.php", - "s4e846bd588a8": "https://laravel.com/framework/docs/12.x/configuration", - "s6510c1aa8d96": "https://laravel.com/framework/docs/12.x/deployment", - "sb3e2d08bd07d": "https://laravel.com/framework/docs/12.x/encryption", - "s879d69d64852": "https://laravel.com/framework/docs/12.x/filesystem", - "sc58ba221e3cd": "https://laravel.com/framework/docs/12.x/fortify", - "sdf4ba04ddcda": "https://laravel.com/framework/docs/12.x/hashing", - "s517839d232ec": "https://laravel.com/framework/docs/12.x/requests", - "s248fef722141": "https://laravel.com/framework/docs/12.x/routing", - "s003198079bdd": "https://laravel.com/framework/docs/12.x/socialite", - "sb3e846095c54": "https://laravel.com/framework/docs/12.x/starter-kits", - "sd7d2287b5d35": "https://laravel.com/framework/docs/12.x/telescope" - } - }, - "debugbar": { - "name": "Laravel Debugbar", - "basis": "unknown", - "sources": { - "s7099781fdf4a": "https://github.com/fruitcake/laravel-debugbar" - } - }, - "fail2ban": { - "name": "fail2ban", - "basis": "1.1.1", - "sources": { - "s0470655e109b": "https://github.com/fail2ban/fail2ban/blob/1.1.1/man/jail.conf.5#L187-L200", - "s9b3b926fe020": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/jail.conf#L378-L384", - "sdb5f6f355db6": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/filter.d/nginx-limit-req.conf#L48-L49" - } - } - }, - "claims": { - "fpm-private": {"text": "FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "fpm-clients": {"text": "listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "fpm-socket": {"text": "Unix socket access uses listen.owner, listen.group and listen.mode, default 0660.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "cookie-secure": {"text": "Native session.cookie_secure defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "cookie-httponly": {"text": "Native session.cookie_httponly defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "cookie-samesite": {"text": "Native session.cookie_samesite defaults empty; choose Lax or Strict.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-strict": {"text": "session.use_strict_mode defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-regenerate": {"text": "Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion.", "components": ["php"], "sources": ["php:s625cd1d40655"], "status": "REASONED"}, - "passwords": {"text": "PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes.", "components": ["php"], "sources": ["php:s50f85ff4fbd1"], "status": "REASONED"}, - "plain-auth": {"text": "Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here.", "components": ["php", "fail2ban"], "sources": ["php:s50f85ff4fbd1", "php:s8664dc56a028", "fail2ban:s0470655e109b", "fail2ban:s9b3b926fe020", "fail2ban:sdb5f6f355db6"], "status": "REASONED"}, - "laravel-debug": {"text": "Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, - "laravel-key": {"text": "Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation.", "components": ["laravel"], "sources": ["laravel:sb3e2d08bd07d"], "status": "REASONED"}, - "laravel-secure": {"text": "Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, - "laravel-httponly": {"text": "SESSION_HTTP_ONLY defaults true; retain it.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, - "laravel-samesite": {"text": "SESSION_SAME_SITE defaults lax; strict suits admin-only apps.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, - "laravel-proxy": {"text": "Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, - "laravel-urls": {"text": "URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs.", "components": ["laravel"], "sources": ["laravel:sf0448f3d4665"], "status": "REASONED"}, - "laravel-hashes": {"text": "Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms.", "components": ["laravel"], "sources": ["laravel:sdf4ba04ddcda"], "status": "REASONED"}, - "laravel-limiter": {"text": "Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route.", "components": ["laravel"], "sources": ["laravel:s248fef722141"], "status": "REASONED"}, - "fortify-login": {"text": "12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, - "fortify-mfa": {"text": "Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, - "socialite": {"text": "Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks.", "components": ["laravel"], "sources": ["laravel:s003198079bdd"], "status": "REASONED"}, - "laravel-cache": {"text": "Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, - "laravel-hosts": {"text": "Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, - "debug-tools": {"text": "Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration.", "components": ["laravel", "debugbar"], "sources": ["laravel:sd7d2287b5d35", "debugbar:s7099781fdf4a"], "status": "REASONED"}, - "curl-peer": {"text": "CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, - "curl-host": {"text": "CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, - "display-errors": {"text": "PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "startup-errors": {"text": "display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "log-errors": {"text": "log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "error-reporting": {"text": "error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "php-banner": {"text": "expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s7b2c2c19835d"], "status": "REASONED"}, - "production-template": {"text": "PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled.", "components": ["template84"], "sources": ["template84:sd72e3d69551f"], "status": "REASONED"}, - "disable-functions": {"text": "disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "url-fopen": {"text": "allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, - "url-include": {"text": "allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, - "basedir": {"text": "open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "fpm-extensions": {"text": "PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "upload-execution": {"text": "Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks.", "components": ["php8", "laravel"], "sources": ["php8:s2e692b7c86a6", "laravel:s879d69d64852"], "status": "REASONED"}, - "laravel-storage": {"text": "Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution.", "components": ["laravel"], "sources": ["laravel:s879d69d64852"], "status": "REASONED"}, - "upload-size": {"text": "upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "post-size": {"text": "post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "upload-count": {"text": "max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s3cbc65590d52"], "status": "REASONED"}, - "memory-limit": {"text": "memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "input-vars": {"text": "max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, - "execution-time": {"text": "max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, - "input-time": {"text": "max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds.", "components": ["php8", "template84"], "sources": ["php8:s5a3f7bde8338", "template84:sd72e3d69551f"], "status": "REASONED"}, - "upload-time": {"text": "Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting.", "components": ["php"], "sources": ["php:s3cbc65590d52"], "status": "REASONED"}, - "fpm-timeout": {"text": "request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "fpm-finished": {"text": "request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "session-gc": {"text": "Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes.", "components": ["php8", "php"], "sources": ["php8:s000e155a7b25", "php:s8664dc56a028"], "status": "REASONED"}, - "session-lifetime": {"text": "Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED"}, - "session-only-cookies": {"text": "session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-storage": {"text": "Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-sid-length": {"text": "PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, - "session-sid-bits": {"text": "PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, - "verify-listener": {"text": "Inventory all FPM listeners: Unix socket or loopback TCP 9000 only.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED", "verify": [1]}, - "verify-tls": {"text": "The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-cookies": {"text": "Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd"], "status": "REASONED", "verify": [2]}, - "verify-config": {"text": "Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo.", "components": ["php", "php8"], "sources": ["php:s5840a5a0a848", "php8:s89c799077b82", "php8:s2e692b7c86a6"], "status": "REASONED", "verify": [3]}, - "verify-banner": {"text": "Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection.", "components": ["php"], "sources": ["php:s7b2c2c19835d", "php:s2f1c806d037c"], "status": "REASONED", "verify": [4]}, - "verify-error": {"text": "Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested.", "components": ["php8", "php"], "sources": ["php8:s89c799077b82", "php:s2f1c806d037c"], "status": "REASONED", "verify": [5]}, - "verify-laravel-error": {"text": "Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED", "verify": [5]}, - "verify-curl-version": {"text": "The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1, 2, 4, 5]} - } -} ---- # PHP and Laravel: 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): - -- php: PHP documentation (unknown) -- curl: curl manual (7.75.0) -- php8: PHP native configuration (8.x) -- template84: PHP production template (8.4) -- template83: PHP production template (8.3) -- laravel: Laravel (12.x) -- debugbar: Laravel Debugbar (unknown) -- fail2ban: fail2ban (1.1.1) +Written against PHP native configuration 8.x, PHP production template 8.4 and 8.3, Laravel 12.x, fail2ban 1.1.1, and curl manual 7.75.0; 2 further sources carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 65 claims, all reasoned from cited sources. -Claims (full source mapping in the front matter): - -- fpm-private (REASONED; php8): FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM. -- fpm-clients (REASONED; php8): listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP. -- fpm-socket (REASONED; php8): Unix socket access uses listen.owner, listen.group and listen.mode, default 0660. -- cookie-secure (REASONED; php8): Native session.cookie_secure defaults 0; enable it. -- cookie-httponly (REASONED; php8): Native session.cookie_httponly defaults 0; enable it. -- cookie-samesite (REASONED; php8): Native session.cookie_samesite defaults empty; choose Lax or Strict. -- session-strict (REASONED; php8): session.use_strict_mode defaults 0; enable it. -- session-regenerate (REASONED; php): Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion. -- passwords (REASONED; php): PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes. -- plain-auth (REASONED; php, fail2ban): Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here. -- laravel-debug (REASONED; laravel): Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values. -- laravel-key (REASONED; laravel): Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation. -- laravel-secure (REASONED; laravel): Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default. -- laravel-httponly (REASONED; laravel): SESSION_HTTP_ONLY defaults true; retain it. -- laravel-samesite (REASONED; laravel): SESSION_SAME_SITE defaults lax; strict suits admin-only apps. -- laravel-proxy (REASONED; laravel): Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability. -- laravel-urls (REASONED; laravel): URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs. -- laravel-hashes (REASONED; laravel): Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms. -- laravel-limiter (REASONED; laravel): Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route. -- fortify-login (REASONED; laravel): 12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views. -- fortify-mfa (REASONED; laravel): Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes. -- socialite (REASONED; laravel): Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks. -- laravel-cache (REASONED; laravel): Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files. -- laravel-hosts (REASONED; laravel): Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false. -- debug-tools (REASONED; laravel, debugbar): Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration. -- curl-peer (REASONED; php): CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification. -- curl-host (REASONED; php): CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA. -- display-errors (REASONED; php8): PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running. -- startup-errors (REASONED; php8): display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off. -- log-errors (REASONED; php8): log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger. -- error-reporting (REASONED; php8): error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ. -- php-banner (REASONED; php8, php): expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification. -- production-template (REASONED; template84): PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled. -- disable-functions (REASONED; php8): disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary. -- url-fopen (REASONED; php): allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy. -- url-include (REASONED; php): allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen. -- basedir (REASONED; php8): open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components. -- fpm-extensions (REASONED; php8): PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server. -- upload-execution (REASONED; php8, laravel): Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks. -- laravel-storage (REASONED; laravel): Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution. -- upload-size (REASONED; php8): upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead. -- post-size (REASONED; php8): post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app. -- upload-count (REASONED; php8, php): max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit. -- memory-limit (REASONED; php8): memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound. -- input-vars (REASONED; php8): max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit. -- execution-time (REASONED; php8): max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline. -- input-time (REASONED; php8, template84): max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds. -- upload-time (REASONED; php): Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting. -- fpm-timeout (REASONED; php8): request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers. -- fpm-finished (REASONED; php8): request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work. -- session-gc (REASONED; php8, php): Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes. -- session-lifetime (REASONED; php8, laravel): Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives. -- session-only-cookies (REASONED; php8): session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4. -- session-storage (REASONED; php8): Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities. -- session-sid-length (REASONED; php8, template83): PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26. -- session-sid-bits (REASONED; php8, template83): PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5. -- verify-listener (REASONED; php8): Inventory all FPM listeners: Unix socket or loopback TCP 9000 only. -- verify-tls (REASONED; curl): The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration. -- verify-cookies (REASONED; php8, laravel): Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax. -- verify-auth (REASONED; laravel): Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication. -- verify-config (REASONED; php, php8): Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo. -- verify-banner (REASONED; php): Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection. -- verify-error (REASONED; php8, php): Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested. -- verify-laravel-error (REASONED; laravel): Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors. -- verify-curl-version (REASONED; curl): The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual. - +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. PHP normally runs behind a web server (Apache with php-fpm or mod_php, nginx or Caddy with php-fpm), so TLS terminates there: follow [apache.md](apache.md), [nginx.md](nginx.md), or [caddy.md](caddy.md) with a certificate from [free-certificates.md](free-certificates.md). The PHP-specific exposures are the php-fpm FastCGI socket (the PHP manual: "An exposed FastCGI endpoint allows arbitrary code execution"), session cookies that PHP ships without `Secure`, `HttpOnly`, or `SameSite` (all three default off), `APP_DEBUG=true` left on in production, and cURL calls with certificate checks turned off. @@ -586,3 +344,271 @@ REASONED: Laravel exception-route comparison. A live Laravel deployment stand-up - Laravel 12.x file storage (private local disk and public storage link): https://laravel.com/framework/docs/12.x/filesystem - Laravel 12.x Telescope local-only installation: https://laravel.com/framework/docs/12.x/telescope - Laravel Debugbar production-installation guidance: https://github.com/fruitcake/laravel-debugbar + + + +## 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/php.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): + +- php8: PHP native configuration (8.x) +- template84: PHP production template (8.4) +- template83: PHP production template (8.3) +- laravel: Laravel (12.x) +- php: PHP documentation (unknown) +- debugbar: Laravel Debugbar (unknown) +- fail2ban: fail2ban (1.1.1) +- curl: curl manual (7.75.0) + +Claims (full source mapping in the JSON block below): + +- fpm-private (REASONED; php8): FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM. +- fpm-clients (REASONED; php8): listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP. +- fpm-socket (REASONED; php8): Unix socket access uses listen.owner, listen.group and listen.mode, default 0660. +- cookie-secure (REASONED; php8): Native session.cookie_secure defaults 0; enable it. +- cookie-httponly (REASONED; php8): Native session.cookie_httponly defaults 0; enable it. +- cookie-samesite (REASONED; php8): Native session.cookie_samesite defaults empty; choose Lax or Strict. +- session-strict (REASONED; php8): session.use_strict_mode defaults 0; enable it. +- session-regenerate (REASONED; php): Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion. +- passwords (REASONED; php): PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes. +- plain-auth (REASONED; php, fail2ban): Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here. +- laravel-debug (REASONED; laravel): Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values. +- laravel-key (REASONED; laravel): Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation. +- laravel-secure (REASONED; laravel): Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default. +- laravel-httponly (REASONED; laravel): SESSION_HTTP_ONLY defaults true; retain it. +- laravel-samesite (REASONED; laravel): SESSION_SAME_SITE defaults lax; strict suits admin-only apps. +- laravel-proxy (REASONED; laravel): Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability. +- laravel-urls (REASONED; laravel): URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs. +- laravel-hashes (REASONED; laravel): Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms. +- laravel-limiter (REASONED; laravel): Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route. +- fortify-login (REASONED; laravel): 12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views. +- fortify-mfa (REASONED; laravel): Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes. +- socialite (REASONED; laravel): Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks. +- laravel-cache (REASONED; laravel): Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files. +- laravel-hosts (REASONED; laravel): Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false. +- debug-tools (REASONED; laravel, debugbar): Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration. +- curl-peer (REASONED; php): CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification. +- curl-host (REASONED; php): CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA. +- display-errors (REASONED; php8): PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running. +- startup-errors (REASONED; php8): display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off. +- log-errors (REASONED; php8): log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger. +- error-reporting (REASONED; php8): error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ. +- php-banner (REASONED; php8, php): expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification. +- production-template (REASONED; template84): PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled. +- disable-functions (REASONED; php8): disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary. +- url-fopen (REASONED; php): allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy. +- url-include (REASONED; php): allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen. +- basedir (REASONED; php8): open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components. +- fpm-extensions (REASONED; php8): PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server. +- upload-execution (REASONED; php8, laravel): Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks. +- laravel-storage (REASONED; laravel): Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution. +- upload-size (REASONED; php8): upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead. +- post-size (REASONED; php8): post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app. +- upload-count (REASONED; php8, php): max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit. +- memory-limit (REASONED; php8): memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound. +- input-vars (REASONED; php8): max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit. +- execution-time (REASONED; php8): max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline. +- input-time (REASONED; php8, template84): max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds. +- upload-time (REASONED; php): Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting. +- fpm-timeout (REASONED; php8): request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers. +- fpm-finished (REASONED; php8): request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work. +- session-gc (REASONED; php8, php): Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes. +- session-lifetime (REASONED; php8, laravel): Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives. +- session-only-cookies (REASONED; php8): session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4. +- session-storage (REASONED; php8): Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities. +- session-sid-length (REASONED; php8, template83): PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26. +- session-sid-bits (REASONED; php8, template83): PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5. +- verify-listener (REASONED; php8): Inventory all FPM listeners: Unix socket or loopback TCP 9000 only. +- verify-tls (REASONED; curl): The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration. +- verify-cookies (REASONED; php8, laravel): Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax. +- verify-auth (REASONED; laravel): Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication. +- verify-config (REASONED; php, php8): Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo. +- verify-banner (REASONED; php): Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection. +- verify-error (REASONED; php8, php): Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested. +- verify-laravel-error (REASONED; laravel): Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors. +- verify-curl-version (REASONED; curl): The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "b65232e7b90f8edc69a6b0b2639a04b3f7b6b4590b7015c4d1d84cd70d5f9aae", + "components": { + "php8": { + "name": "PHP native configuration", + "basis": "8.x", + "sources": { + "s89c799077b82": "https://www.php.net/manual/en/errorfunc.configuration.php", + "s5a3f7bde8338": "https://www.php.net/manual/en/info.configuration.php", + "sc9e417e2ad6f": "https://www.php.net/manual/en/ini.core.php", + "s2e692b7c86a6": "https://www.php.net/manual/en/install.fpm.configuration.php", + "s000e155a7b25": "https://www.php.net/manual/en/session.configuration.php" + } + }, + "template84": { + "name": "PHP production template", + "basis": "8.4", + "sources": { + "sd72e3d69551f": "https://raw.githubusercontent.com/php/php-src/d313ad6098430f4e61f0121a9e7ab392d195e4e4/php.ini-production" + } + }, + "template83": { + "name": "PHP production template", + "basis": "8.3", + "sources": { + "sea3d3a4ea486": "https://raw.githubusercontent.com/php/php-src/b94f9f68a610e0fca5f2fea5bfa7c7d6d3d5a847/php.ini-production" + } + }, + "laravel": { + "name": "Laravel", + "basis": "12.x", + "sources": { + "sf0448f3d4665": "https://api.laravel.com/docs/12.x/Illuminate/Routing/UrlGenerator.html", + "s98db3429151c": "https://github.com/laravel/laravel/blob/f6b2e79bdbfc5bf4a37ad16466cc06ad79cc9e8f/config/session.php", + "s4e846bd588a8": "https://laravel.com/framework/docs/12.x/configuration", + "s6510c1aa8d96": "https://laravel.com/framework/docs/12.x/deployment", + "sb3e2d08bd07d": "https://laravel.com/framework/docs/12.x/encryption", + "s879d69d64852": "https://laravel.com/framework/docs/12.x/filesystem", + "sc58ba221e3cd": "https://laravel.com/framework/docs/12.x/fortify", + "sdf4ba04ddcda": "https://laravel.com/framework/docs/12.x/hashing", + "s517839d232ec": "https://laravel.com/framework/docs/12.x/requests", + "s248fef722141": "https://laravel.com/framework/docs/12.x/routing", + "s003198079bdd": "https://laravel.com/framework/docs/12.x/socialite", + "sb3e846095c54": "https://laravel.com/framework/docs/12.x/starter-kits", + "sd7d2287b5d35": "https://laravel.com/framework/docs/12.x/telescope" + } + }, + "php": { + "name": "PHP documentation", + "basis": "unknown", + "sources": { + "s09fdbeba0867": "https://www.php.net/manual/en/curl.constants.php", + "s5840a5a0a848": "https://www.php.net/manual/en/features.commandline.options.php", + "s3cbc65590d52": "https://www.php.net/manual/en/features.file-upload.common-pitfalls.php", + "s6f3e55d536e7": "https://www.php.net/manual/en/filesystem.configuration.php", + "s50f85ff4fbd1": "https://www.php.net/manual/en/function.password-hash.php", + "s625cd1d40655": "https://www.php.net/manual/en/function.session-regenerate-id.php", + "s2f1c806d037c": "https://www.php.net/manual/en/function.trigger-error.php", + "s7b2c2c19835d": "https://www.php.net/manual/en/security.hiding.php", + "s8664dc56a028": "https://www.php.net/manual/en/session.security.ini.php" + } + }, + "debugbar": { + "name": "Laravel Debugbar", + "basis": "unknown", + "sources": { + "s7099781fdf4a": "https://github.com/fruitcake/laravel-debugbar" + } + }, + "fail2ban": { + "name": "fail2ban", + "basis": "1.1.1", + "sources": { + "s0470655e109b": "https://github.com/fail2ban/fail2ban/blob/1.1.1/man/jail.conf.5#L187-L200", + "s9b3b926fe020": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/jail.conf#L378-L384", + "sdb5f6f355db6": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/filter.d/nginx-limit-req.conf#L48-L49" + } + }, + "curl": { + "name": "curl manual", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + } + }, + "claims": { + "fpm-private": {"text": "FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-clients": {"text": "listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-socket": {"text": "Unix socket access uses listen.owner, listen.group and listen.mode, default 0660.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "cookie-secure": {"text": "Native session.cookie_secure defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "cookie-httponly": {"text": "Native session.cookie_httponly defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "cookie-samesite": {"text": "Native session.cookie_samesite defaults empty; choose Lax or Strict.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-strict": {"text": "session.use_strict_mode defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-regenerate": {"text": "Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion.", "components": ["php"], "sources": ["php:s625cd1d40655"], "status": "REASONED"}, + "passwords": {"text": "PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes.", "components": ["php"], "sources": ["php:s50f85ff4fbd1"], "status": "REASONED"}, + "plain-auth": {"text": "Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here.", "components": ["php", "fail2ban"], "sources": ["php:s50f85ff4fbd1", "php:s8664dc56a028", "fail2ban:s0470655e109b", "fail2ban:s9b3b926fe020", "fail2ban:sdb5f6f355db6"], "status": "REASONED"}, + "laravel-debug": {"text": "Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, + "laravel-key": {"text": "Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation.", "components": ["laravel"], "sources": ["laravel:sb3e2d08bd07d"], "status": "REASONED"}, + "laravel-secure": {"text": "Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-httponly": {"text": "SESSION_HTTP_ONLY defaults true; retain it.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-samesite": {"text": "SESSION_SAME_SITE defaults lax; strict suits admin-only apps.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-proxy": {"text": "Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, + "laravel-urls": {"text": "URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs.", "components": ["laravel"], "sources": ["laravel:sf0448f3d4665"], "status": "REASONED"}, + "laravel-hashes": {"text": "Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms.", "components": ["laravel"], "sources": ["laravel:sdf4ba04ddcda"], "status": "REASONED"}, + "laravel-limiter": {"text": "Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route.", "components": ["laravel"], "sources": ["laravel:s248fef722141"], "status": "REASONED"}, + "fortify-login": {"text": "12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, + "fortify-mfa": {"text": "Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, + "socialite": {"text": "Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks.", "components": ["laravel"], "sources": ["laravel:s003198079bdd"], "status": "REASONED"}, + "laravel-cache": {"text": "Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, + "laravel-hosts": {"text": "Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, + "debug-tools": {"text": "Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration.", "components": ["laravel", "debugbar"], "sources": ["laravel:sd7d2287b5d35", "debugbar:s7099781fdf4a"], "status": "REASONED"}, + "curl-peer": {"text": "CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, + "curl-host": {"text": "CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, + "display-errors": {"text": "PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "startup-errors": {"text": "display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "log-errors": {"text": "log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "error-reporting": {"text": "error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "php-banner": {"text": "expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s7b2c2c19835d"], "status": "REASONED"}, + "production-template": {"text": "PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled.", "components": ["template84"], "sources": ["template84:sd72e3d69551f"], "status": "REASONED"}, + "disable-functions": {"text": "disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "url-fopen": {"text": "allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, + "url-include": {"text": "allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, + "basedir": {"text": "open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "fpm-extensions": {"text": "PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "upload-execution": {"text": "Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks.", "components": ["php8", "laravel"], "sources": ["php8:s2e692b7c86a6", "laravel:s879d69d64852"], "status": "REASONED"}, + "laravel-storage": {"text": "Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution.", "components": ["laravel"], "sources": ["laravel:s879d69d64852"], "status": "REASONED"}, + "upload-size": {"text": "upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "post-size": {"text": "post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "upload-count": {"text": "max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s3cbc65590d52"], "status": "REASONED"}, + "memory-limit": {"text": "memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "input-vars": {"text": "max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, + "execution-time": {"text": "max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, + "input-time": {"text": "max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds.", "components": ["php8", "template84"], "sources": ["php8:s5a3f7bde8338", "template84:sd72e3d69551f"], "status": "REASONED"}, + "upload-time": {"text": "Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting.", "components": ["php"], "sources": ["php:s3cbc65590d52"], "status": "REASONED"}, + "fpm-timeout": {"text": "request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-finished": {"text": "request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "session-gc": {"text": "Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes.", "components": ["php8", "php"], "sources": ["php8:s000e155a7b25", "php:s8664dc56a028"], "status": "REASONED"}, + "session-lifetime": {"text": "Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED"}, + "session-only-cookies": {"text": "session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-storage": {"text": "Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-sid-length": {"text": "PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, + "session-sid-bits": {"text": "PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, + "verify-listener": {"text": "Inventory all FPM listeners: Unix socket or loopback TCP 9000 only.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-cookies": {"text": "Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd"], "status": "REASONED", "verify": [2]}, + "verify-config": {"text": "Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo.", "components": ["php", "php8"], "sources": ["php:s5840a5a0a848", "php8:s89c799077b82", "php8:s2e692b7c86a6"], "status": "REASONED", "verify": [3]}, + "verify-banner": {"text": "Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection.", "components": ["php"], "sources": ["php:s7b2c2c19835d", "php:s2f1c806d037c"], "status": "REASONED", "verify": [4]}, + "verify-error": {"text": "Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested.", "components": ["php8", "php"], "sources": ["php8:s89c799077b82", "php:s2f1c806d037c"], "status": "REASONED", "verify": [5]}, + "verify-laravel-error": {"text": "Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED", "verify": [5]}, + "verify-curl-version": {"text": "The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1, 2, 4, 5]} + } +} +```` + diff --git a/plugin/plugin.json b/plugin/plugin.json index a430b812c..d00e54e51 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "secureconfig", - "version": "1.0.697", + "version": "1.0.699", "description": "Deployment-exposure guides for services AI-assisted projects commonly run: TLS, authentication, MFA, secrets, and network exposure, each line traced to a vendor page.", "author": { "name": "Jeff Posluns", "url": "https://secureconfig.ai" }, "homepage": "https://secureconfig.ai", diff --git a/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 b/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 index 3184eab00..3f872a5c3 100644 --- a/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 +++ b/plugin/skills/secure-deployment-config/references/MANIFEST.sha256 @@ -6,7 +6,7 @@ b76f678010dfbb1eda11cb17174c28a07ac28cfbee453c9bf16c5e72ac2eb38b tailscale.md 267a1cfb8144a162281e7ccf80f3d25464cceeb1459d387ae165e7924a91b9ea tunnels.md 5540325c9d47ef3110273bed4807893df9359b32dfe49091f403eda60576a755 authentication.md -7ae3e7e3e685f7a1f9145175c07ad602d0e4426f1c1b09b56cc9287cf91fe4a8 mfa.md +d5417d828cff8a2fc0da5bde3850ef2eb0b2e86c77e43000612c434b4f86dbcc mfa.md 998211fa469d26e685293914ab164b3be8ef7d0826ea5353903c5704899e36b7 secrets.md 67ead6128e8115fe289b11db4010b4fc046b44fbf797bb7d3ac6c52f4e4d4959 vault.md d18b415d444bcccc6ffdf29affcf52a7ed8798acd780897473f7c5db8866d8b1 identity-providers.md @@ -22,25 +22,25 @@ e294462aeb6ce1498c97d8198598f79b3b8d7c0930fad5538e687587cf76887b lighttpd.md 123465d2fd41e0e4c7017bd8d71ca4ae11fc4df11110773ffac4080bc447c7ee haproxy.md 3ae6e4a8a38c6bd114821364c11b50468595278ffba1ac0715651a4d634325af traefik.md 96b7ed57a8c12a77f9dc8e0a26695bc127ae6947973d88ce01bd46e10797af3d nodejs.md -5fd9b83e3bec71386ffa2da831a70c57e0225aa1ca3d969a0737b8a28aaf5e62 python.md +c609d3d870923e9276f8f4485957e8f689894290b324fad2637fb6fea8810bf0 python.md d03313bfa0afb1e9a960c070516b5f5f1d328d7b41aba215b52619f328395138 docker.md 1b8445e0dc25fb843829d1b2fdc2c67ee64b1e63b4a89b8518f1a994e722143f kubernetes.md 7c35f943796e0cfcba3c327a1c7c6e3dba385de4d90000a64ceaf09da81e5746 gitops-controllers.md 4d831688e4658d37dd10f2729397f3d225fdac03d8861f20a850a8ffc9962662 self-hosted-ci-runners.md -b6e114b214938bb50a6bf20beb8ff87eab438a61bb328cc40abda0341d8e507e frontend-frameworks.md +8821c15b9a8007190be74118d90f349d2e87790908fa4bcbc2d4d6ed874dd1fa frontend-frameworks.md 3cc39b499774d04e366810d1aedd7e9c88dcf2534040d81c021d270bf37c2fb4 container-hardening.md -b649f885ec96782415e287e83312fbd9444d0e70ab99467f477fcf9d29196671 nextjs.md +6d4ac36548b0de98382fc5003d0577d16be86535957f72dd33176234add90028 nextjs.md 830424f7d66e885eb088a20c42bd60bed0cea493cb788f72d53785cf652f39de go.md -d3c5aa76a95c369cc8a3b4c16120d68cf5fa1512d1d88a3c9e3d2ddb27ed52fa dotnet.md +dff5cfe166a59c4d384dd977fab5426b0c7c8bfb5b6125765c80d7a7c7ef1b88 dotnet.md 483c9fe8e28df277628817218e8b295403be9eeedf376adbe377cfc2abd6c118 java.md -173891313b0f01bd14f8eb26af516b5dbaf55673a28ca5cf6eaa546d5570451e php.md -977560c6589018542f1308b6de38df7290fe3cf738355ced74e3ff2a18cf140b ruby.md +f1bd449090c1f3c159d7d3d6a0b44d49c742c42d41b9a08fbed94937b2220255 php.md +ad47380702f2c803e066c7d89bd2e2d311693f22493875ee93f285ec96ca2e15 ruby.md a7f51f22878598d67c63943bba9957231f5d5c2d99324994fdd6366d06b9a86d host.md 499ddd36b87b44ed676ab6647a26e689c6c0efe8cf3794dab9131709edf02111 cloud-firewalls.md -019c41e1b1744d00cb17d7e8cef73207340f78b86f665fd16a069e660d64c935 paas.md +860e20169175feda8cbce73ffdf164d77ca625cbb681713971397ac263290346 paas.md 17091cd4ef3b1c9b8ea0be22fa019f07cf02e581b76d7b8506eba1bd959a052b egress-metadata.md -53b988a119bebc360c9d2bb6065be38c8d034a53b24fea591acf3e86ca44fa69 gpu-clouds.md -64ff7d2b9ef85c24b13099092fc9ff185d7d66a09447cfb24e12ec19e24dba00 deployment-lifecycle.md +2aaa5a1425d394ebd6464a40555dd89a22a91ecd3354b9141d60cc7fa0dc8b6d gpu-clouds.md +d393dc7e920ff1649154d05910dbf2fb0c577d198a854495ecb8d089492ac191 deployment-lifecycle.md 607338a9c1aa23484131619211818491966e30a064f2b7b2fe9a343af31918bf postgresql.md da990e0ca7e7b950deb35fdd859677ef247db8f2aa8ca34330a857708f8830ca connection-poolers.md 80360a609312bb18f0f7a2740de2b003d996061721f8a964a9502141506e1e27 mysql.md @@ -57,8 +57,8 @@ e83781f3748295c4fba70c33f681d3942cf14d8717649657f7caea45af9008e0 memcached.md 5543c2465f6cde11ce3a0d48e81fa8c82ba045d01133bbb5f98704cada4db29c object-storage.md 8d6d98a48b2cc034ad28b70c5dabad9031c0e265d2ad9fabcc9946feb45b1af1 nats.md e96fc9a821530c14fd174dcb8cec56c597c6c9d9bc13e5b0b7142fbf889518f8 search-engines.md -a96c9118e46b0b24bd1e406a063bd6efd109d617df2f19218991fa8e618fed5c sqlite.md -bef75373cbe7e5b66c0a8becf84cd3ac1d5894d12ade49f9688ed00fbcaff229 sqlite-http-frontends.md +645f6a5783b9d0d36aa3774aba966eda27f65f6713d62a0e2256c5c8f74e27ea sqlite.md +d7ebebff08b1ceee79247dd430582ca702015395ad0fc755c30a4e5386c0b4f2 sqlite-http-frontends.md 39d509c156f6486b122cc2a5bb4da6ca6ffd7a486afd38cf83066a18e9e2df5c surrealdb.md 5e31e69fdbe931bb373d1ba1aaf1ce65575210466dbce873b93e465111ae761e time-series-metrics-stores.md f8c3823a059b3641dfe2db85a8510fd2eeb0d7fa0c470e623d1700627dc74186 jupyter.md @@ -75,7 +75,7 @@ cae5895e27a2a0d08773bf3c6f346c25deee35db50f15cfaaceff1c2545eba3e self-hosted-er 75560b7ae60244f00675674a45f3aaa1c715da1adb93f37334181d35924af9e4 mcp-servers.md 86c0dcec778ea45f122f458981177bbe1fe089867183613939570b2fde1fdff5 mcp-clients.md 4ea7c2c7bc490c32fac988a7eca86353dd3b9edeca51bc805c4752e958bbb5e7 ray.md -0d4ebac99993632db53c1a3c835bf0cc8f113d689c2e1884b317b1427570651c mlflow.md +707a93ac9d3f9a15c7f716aa7d3b8c5c3067ebbd6cabe6777b232fae60aad4db mlflow.md b772a966100db0e2b6f1f7ba78903f1dde4fd6e351b58afb752124e55440681e agent-builders.md f205ad0512c2ad45bee20f9cce6faee7eaadddc16af066c413eee316ec79efa9 ai-infra-services.md 5448efdd90dc5fdf90429e23ef8b8dbad8cad33a36b2d056ca426a63b4d56096 realtime-voice-infra.md @@ -83,19 +83,19 @@ f205ad0512c2ad45bee20f9cce6faee7eaadddc16af066c413eee316ec79efa9 ai-infra-servi eedd2df7ea2844e3500579dddd9a18e57c746b7f49616bc76d9cdad0d20e235e chat-uis.md de45069e092247a3e60962e15ae9e775cfcc6520a983aaf91699332f94816311 llm-observability.md bf00fe82a4905fa71a3a8aaf534ab7e60f5ee342259d6c7b910468057d1a9d60 workflow-orchestrators.md -6e808e21563913691d5e9b7f7b0ef457ad332ea9655a9e0a73bd454e4c27cc62 admin-uis.md +f09c2c0f0f00dc39f8cb42b3067220b8baddd997a4226f3b66ca26eb735d071f admin-uis.md bd72febbec4b4aba89c866898fb3bc6f821985de356f733fdea356637f512cd8 observability-components.md -8d988fc846fb6a195061ad09ae70c9ebb50654ae8f499364086233554f5163d2 devops-uis.md +59a50514647c171508ee697dafa4114f68e13d5d75053a76c3ec6b6ef09635bb devops-uis.md 96807f276db704f93b2cf9f6a74462372e306bc727041a405e64a78e57e85dc8 cors.md b342da74961426bb8757064ffadaace7a802f7ea9c90d357840821f2c724918f headers.md 7f6a4be63fe75218a7290360d7fa05e4f3eed04e759fd2279b8f9090ef8969d4 firebase-supabase.md 14a0db1c936d383f538b6c4be91a626020969dbd6bc5c59bafec40cdc6a8aa34 supabase-self-hosted.md -e4ba632a7d23127a50f3ca43d06026acf696975a7eea07a4afd50709e6c41ebc web-exposure.md +1b08c02dc15252340becfa270cefde0f8381b4cff68565b69b9c94dd72ed33c4 web-exposure.md d47b8df006d058984f8d30c5fd9345fddb9dd5449d14da83047d0d1338d2be7c realtime-webhooks.md -1950768370917b08e8933c138b68c762b99dcc43bf3aa7059b23523d8504d61a transactional-email-posture.md -c899ee328d6850c78dca030d5bb4253f3e5f037186e14bff89077992d37d11d8 bi-dashboards.md -b336702b0aab97bc2e77ea0a67d20910f675b6c9f16b722f6e01b5dd0ada1b58 low-code-builders.md -a8ee9cb4733666d9c13b241a92abe17df75a070295bb46a10eca9da7b17a813e server-admin-panels.md +72d9c43f73e6b0fc6d0b49b0b305cad98c7aa7019bfc9c50d993373269b0de12 transactional-email-posture.md +d474e2e64d33e33d8556bf64fc1199df39c1f902e4514dbd9e8c46099ac9092d bi-dashboards.md +1b9435b5fbfcd08885235e9f590946cf941922df7a07b081b8afda62be1b701b low-code-builders.md +f2c74dba7a154cfdac2d0af924ab3cdb6fbe5753674c8e97449fdaa4534d3f6b server-admin-panels.md 57e33f986cc7430a583d4fa395fc97b46cc799414b95114a6fc66e7e46c02d26 nomad-consul.md e2ed2544c21ba3583b6940b45e5503dbf7c22d0a2f4dab1b77244fdf203e2d6b pocketbase.md 31edb9401ef4e8f4374e2036f894e47c69d3888ffebb5bc921db05ae37136c52 headless-browser-services.md diff --git a/plugin/skills/secure-deployment-config/references/admin-uis.md b/plugin/skills/secure-deployment-config/references/admin-uis.md index a9d197161..03091dece 100644 --- a/plugin/skills/secure-deployment-config/references/admin-uis.md +++ b/plugin/skills/secure-deployment-config/references/admin-uis.md @@ -1,203 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "92d3d0f412f545709088bb0ce3d9b82c95988187a6fd4fcf75747d818f8d5c8b", - "components": { - "mongo": { - "name": "mongo-express", - "basis": "unknown", - "sources": { - "s69ec4eee2c85": "https://github.com/mongo-express/mongo-express" - } - }, - "grafana": { - "name": "Grafana docs", - "basis": "unknown", - "sources": { - "s7aa447405cc5": "https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/", - "sc241e0772095": "https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/" - } - }, - "prometheus": { - "name": "Prometheus docs", - "basis": "unknown", - "sources": { - "scce074c47e9c": "https://prometheus.io/docs/guides/basic-auth/", - "sc48b506cc29a": "https://prometheus.io/docs/guides/tls-encryption/" - } - }, - "phpmyadmin": { - "name": "phpMyAdmin", - "basis": "unknown", - "sources": { - "s5f6c8965c539": "https://www.phpmyadmin.net/docs/" - } - }, - "pgadmin": { - "name": "pgAdmin", - "basis": "unknown", - "sources": { - "s2f7e33714435": "https://www.pgadmin.org/docs/" - } - }, - "adminer": { - "name": "Adminer", - "basis": "unknown", - "sources": { - "sb5eda9397b18": "https://www.adminer.org/" - } - }, - "prometheus-pin": { - "name": "Prometheus", - "basis": "v3.14.0", - "sources": { - "s6d5f308b02a2": "https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425" - } - }, - "grafana-pin": { - "name": "Grafana", - "basis": "v13.2.2", - "sources": { - "s4c6ff3174e7e": "https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50", - "sb00e234d0ddd": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469", - "sf14b29976adb": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568" - } - }, - "go": { - "name": "Go net", - "basis": "unknown", - "sources": { - "s37204ff1b27c": "https://pkg.go.dev/net#Listen" - } - }, - "htpasswd": { - "name": "Apache htpasswd", - "basis": "2.4", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "httpd-source": { - "name": "Apache httpd htpasswd source", - "basis": "2.4.69", - "sources": { - "sfed14ba2991a": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297", - "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", - "s4ec6b9b216dc": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383", - "s6952d96564d9": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129" - } - }, - "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" - } - }, - "redisinsight": { - "name": "RedisInsight", - "basis": "3.8.0", - "sources": { - "s6b8fb1c9bbbc": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100", - "s7ba57be16bb3": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133", - "sddf9b5ee80b9": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49", - "s5e6e219b4b65": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35" - } - }, - "engine-pinned": { - "name": "Docker Engine networking documentation", - "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", - "sources": { - "sb3dacfbaf7e1": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38", - "s1c82ff316a7e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62", - "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" - } - }, - "moby-pinned": { - "name": "Moby port mapping source", - "basis": "v28.3.3", - "sources": { - "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" - } - } - }, - "claims": { - "private": {"text": "Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice.", "components": ["grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer", "redisinsight"], "sources": ["grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18", "redisinsight:s6b8fb1c9bbbc", "redisinsight:s7ba57be16bb3", "redisinsight:sddf9b5ee80b9", "redisinsight:s5e6e219b4b65"], "status": "REASONED"}, - "mongo-enable": {"text": "Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, - "mongo-credentials": {"text": "Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, - "mongo-database": {"text": "Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, - "grafana-admin": {"text": "First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, - "grafana-anonymous": {"text": "Disable Grafana anonymous access if enabled.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, - "grafana-mfa": {"text": "Prefer SSO with MFA at the identity provider.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, - "grafana-bind": {"text": "Empty http_addr joins the port and binds all interfaces; set 127.0.0.1.", "components": ["grafana-pin", "go"], "sources": ["grafana-pin:s4c6ff3174e7e", "grafana-pin:sb00e234d0ddd", "grafana-pin:sf14b29976adb", "go:s37204ff1b27c"], "status": "REASONED"}, - "grafana-tls": {"text": "Native HTTPS uses server protocol=https, cert_file and cert_key.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5", "grafana:sc241e0772095"], "status": "REASONED"}, - "prometheus-auth": {"text": "Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes.", "components": ["prometheus"], "sources": ["prometheus:scce074c47e9c"], "status": "REASONED"}, - "prometheus-bcrypt": {"text": "Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt.", "components": ["prometheus", "htpasswd", "httpd-source", "owasp-source"], "sources": ["prometheus:scce074c47e9c", "htpasswd:scde9004bb967", "httpd-source:sfed14ba2991a", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4ec6b9b216dc", "httpd-source:s6952d96564d9", "owasp-source:s4531e65b6502"], "status": "REASONED"}, - "prometheus-bind": {"text": "Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090.", "components": ["prometheus-pin"], "sources": ["prometheus-pin:s6d5f308b02a2"], "status": "REASONED"}, - "prometheus-tls": {"text": "web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config.", "components": ["prometheus"], "sources": ["prometheus:sc48b506cc29a"], "status": "REASONED"}, - "database-panels": {"text": "Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated.", "components": ["phpmyadmin", "pgadmin"], "sources": ["phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435"], "status": "REASONED"}, - "pgadmin-login": {"text": "pgAdmin server mode has its own accounts/login.", "components": ["pgadmin"], "sources": ["pgadmin:s2f7e33714435"], "status": "REASONED"}, - "adminer-login": {"text": "Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, - "adminer-file": {"text": "Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, - "verify-listeners": {"text": "Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy.", "components": ["grafana-pin", "prometheus-pin", "engine-pinned", "moby-pinned"], "sources": ["grafana-pin:s4c6ff3174e7e", "prometheus-pin:s6d5f308b02a2", "engine-pinned:sb3dacfbaf7e1", "engine-pinned:s1c82ff316a7e", "engine-pinned:sc244607b20e4", "moby-pinned:seaf4197aafaa"], "status": "REASONED", "verify": [1]}, - "verify-login": {"text": "Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]}, - "verify-data": {"text": "A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]} - } -} ---- # Admin panels: phpMyAdmin, pgAdmin, mongo-express, Grafana, Prometheus - -**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): - -- mongo: mongo-express (unknown) -- grafana: Grafana docs (unknown) -- prometheus: Prometheus docs (unknown) -- phpmyadmin: phpMyAdmin (unknown) -- pgadmin: pgAdmin (unknown) -- adminer: Adminer (unknown) -- prometheus-pin: Prometheus (v3.14.0) -- grafana-pin: Grafana (v13.2.2) -- go: Go net (unknown) -- htpasswd: Apache htpasswd (2.4) -- httpd-source: Apache httpd htpasswd source (2.4.69) -- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) -- redisinsight: RedisInsight (3.8.0) -- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) -- moby-pinned: Moby port mapping source (v28.3.3) - -Claims (full source mapping in the front matter): +Written against phpMyAdmin (no recorded version), Grafana v13.2.2, Prometheus v3.14.0, RedisInsight 3.8.0, Apache htpasswd 2.4, Apache httpd htpasswd source 2.4.69, and Moby port mapping source v28.3.3; 2 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- private (REASONED; grafana, prometheus, phpmyadmin, pgadmin, adminer, redisinsight): Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice. -- mongo-enable (REASONED; mongo): Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify. -- mongo-credentials (REASONED; mongo): Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults. -- mongo-database (REASONED; mongo): Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL. -- grafana-admin (REASONED; grafana): First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts. -- grafana-anonymous (REASONED; grafana): Disable Grafana anonymous access if enabled. -- grafana-mfa (REASONED; grafana): Prefer SSO with MFA at the identity provider. -- grafana-bind (REASONED; grafana-pin, go): Empty http_addr joins the port and binds all interfaces; set 127.0.0.1. -- grafana-tls (REASONED; grafana): Native HTTPS uses server protocol=https, cert_file and cert_key. -- prometheus-auth (REASONED; prometheus): Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes. -- prometheus-bcrypt (REASONED; prometheus, htpasswd, httpd-source, owasp-source): Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt. -- prometheus-bind (REASONED; prometheus-pin): Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090. -- prometheus-tls (REASONED; prometheus): web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config. -- database-panels (REASONED; phpmyadmin, pgadmin): Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated. -- pgadmin-login (REASONED; pgadmin): pgAdmin server mode has its own accounts/login. -- adminer-login (REASONED; adminer): Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts. -- adminer-file (REASONED; adminer): Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies. -- verify-listeners (REASONED; grafana-pin, prometheus-pin, engine-pinned, moby-pinned): Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy. -- verify-login (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive. -- verify-data (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are 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. Database and monitoring panels are the most-scanned targets on the internet, and several ship with known default credentials. One rule dominates everything tool-specific below: **an admin panel is never reachable from the public internet.** Bind it to loopback and reach it through SSH port forwarding, a VPN or tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) plus MFA ([mfa.md](mfa.md)). @@ -309,3 +114,224 @@ REASONED: preceding block; listener inventory, anonymous front-page/data-route c - Docker conditional 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 Linux firewall rules for publication, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 - Moby port mapping at v28.3.3, conditional proxy startup and TCP listening without a proxy (checked October 2026): https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.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/admin-uis.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): + +- phpmyadmin: phpMyAdmin (unknown) +- grafana-pin: Grafana (v13.2.2) +- prometheus-pin: Prometheus (v3.14.0) +- pgadmin: pgAdmin (unknown) +- mongo: mongo-express (unknown) +- grafana: Grafana docs (unknown) +- prometheus: Prometheus docs (unknown) +- adminer: Adminer (unknown) +- redisinsight: RedisInsight (3.8.0) +- go: Go net (unknown) +- htpasswd: Apache htpasswd (2.4) +- httpd-source: Apache httpd htpasswd source (2.4.69) +- engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) +- moby-pinned: Moby port mapping source (v28.3.3) +- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; grafana, prometheus, phpmyadmin, pgadmin, adminer, redisinsight): Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice. +- mongo-enable (REASONED; mongo): Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify. +- mongo-credentials (REASONED; mongo): Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults. +- mongo-database (REASONED; mongo): Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL. +- grafana-admin (REASONED; grafana): First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts. +- grafana-anonymous (REASONED; grafana): Disable Grafana anonymous access if enabled. +- grafana-mfa (REASONED; grafana): Prefer SSO with MFA at the identity provider. +- grafana-bind (REASONED; grafana-pin, go): Empty http_addr joins the port and binds all interfaces; set 127.0.0.1. +- grafana-tls (REASONED; grafana): Native HTTPS uses server protocol=https, cert_file and cert_key. +- prometheus-auth (REASONED; prometheus): Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes. +- prometheus-bcrypt (REASONED; prometheus, htpasswd, httpd-source, owasp-source): Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt. +- prometheus-bind (REASONED; prometheus-pin): Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090. +- prometheus-tls (REASONED; prometheus): web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config. +- database-panels (REASONED; phpmyadmin, pgadmin): Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated. +- pgadmin-login (REASONED; pgadmin): pgAdmin server mode has its own accounts/login. +- adminer-login (REASONED; adminer): Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts. +- adminer-file (REASONED; adminer): Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies. +- verify-listeners (REASONED; grafana-pin, prometheus-pin, engine-pinned, moby-pinned): Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy. +- verify-login (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive. +- verify-data (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "92d3d0f412f545709088bb0ce3d9b82c95988187a6fd4fcf75747d818f8d5c8b", + "components": { + "phpmyadmin": { + "name": "phpMyAdmin", + "basis": "unknown", + "sources": { + "s5f6c8965c539": "https://www.phpmyadmin.net/docs/" + } + }, + "grafana-pin": { + "name": "Grafana", + "basis": "v13.2.2", + "sources": { + "s4c6ff3174e7e": "https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50", + "sb00e234d0ddd": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469", + "sf14b29976adb": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568" + } + }, + "prometheus-pin": { + "name": "Prometheus", + "basis": "v3.14.0", + "sources": { + "s6d5f308b02a2": "https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425" + } + }, + "pgadmin": { + "name": "pgAdmin", + "basis": "unknown", + "sources": { + "s2f7e33714435": "https://www.pgadmin.org/docs/" + } + }, + "mongo": { + "name": "mongo-express", + "basis": "unknown", + "sources": { + "s69ec4eee2c85": "https://github.com/mongo-express/mongo-express" + } + }, + "grafana": { + "name": "Grafana docs", + "basis": "unknown", + "sources": { + "s7aa447405cc5": "https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/", + "sc241e0772095": "https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/" + } + }, + "prometheus": { + "name": "Prometheus docs", + "basis": "unknown", + "sources": { + "scce074c47e9c": "https://prometheus.io/docs/guides/basic-auth/", + "sc48b506cc29a": "https://prometheus.io/docs/guides/tls-encryption/" + } + }, + "adminer": { + "name": "Adminer", + "basis": "unknown", + "sources": { + "sb5eda9397b18": "https://www.adminer.org/" + } + }, + "redisinsight": { + "name": "RedisInsight", + "basis": "3.8.0", + "sources": { + "s6b8fb1c9bbbc": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100", + "s7ba57be16bb3": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133", + "sddf9b5ee80b9": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49", + "s5e6e219b4b65": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35" + } + }, + "go": { + "name": "Go net", + "basis": "unknown", + "sources": { + "s37204ff1b27c": "https://pkg.go.dev/net#Listen" + } + }, + "htpasswd": { + "name": "Apache htpasswd", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "httpd-source": { + "name": "Apache httpd htpasswd source", + "basis": "2.4.69", + "sources": { + "sfed14ba2991a": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297", + "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", + "s4ec6b9b216dc": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383", + "s6952d96564d9": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129" + } + }, + "engine-pinned": { + "name": "Docker Engine networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sb3dacfbaf7e1": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38", + "s1c82ff316a7e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62", + "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" + } + }, + "moby-pinned": { + "name": "Moby port mapping source", + "basis": "v28.3.3", + "sources": { + "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" + } + }, + "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": { + "private": {"text": "Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice.", "components": ["grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer", "redisinsight"], "sources": ["grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18", "redisinsight:s6b8fb1c9bbbc", "redisinsight:s7ba57be16bb3", "redisinsight:sddf9b5ee80b9", "redisinsight:s5e6e219b4b65"], "status": "REASONED"}, + "mongo-enable": {"text": "Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "mongo-credentials": {"text": "Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "mongo-database": {"text": "Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "grafana-admin": {"text": "First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-anonymous": {"text": "Disable Grafana anonymous access if enabled.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-mfa": {"text": "Prefer SSO with MFA at the identity provider.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-bind": {"text": "Empty http_addr joins the port and binds all interfaces; set 127.0.0.1.", "components": ["grafana-pin", "go"], "sources": ["grafana-pin:s4c6ff3174e7e", "grafana-pin:sb00e234d0ddd", "grafana-pin:sf14b29976adb", "go:s37204ff1b27c"], "status": "REASONED"}, + "grafana-tls": {"text": "Native HTTPS uses server protocol=https, cert_file and cert_key.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5", "grafana:sc241e0772095"], "status": "REASONED"}, + "prometheus-auth": {"text": "Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes.", "components": ["prometheus"], "sources": ["prometheus:scce074c47e9c"], "status": "REASONED"}, + "prometheus-bcrypt": {"text": "Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt.", "components": ["prometheus", "htpasswd", "httpd-source", "owasp-source"], "sources": ["prometheus:scce074c47e9c", "htpasswd:scde9004bb967", "httpd-source:sfed14ba2991a", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4ec6b9b216dc", "httpd-source:s6952d96564d9", "owasp-source:s4531e65b6502"], "status": "REASONED"}, + "prometheus-bind": {"text": "Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090.", "components": ["prometheus-pin"], "sources": ["prometheus-pin:s6d5f308b02a2"], "status": "REASONED"}, + "prometheus-tls": {"text": "web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config.", "components": ["prometheus"], "sources": ["prometheus:sc48b506cc29a"], "status": "REASONED"}, + "database-panels": {"text": "Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated.", "components": ["phpmyadmin", "pgadmin"], "sources": ["phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435"], "status": "REASONED"}, + "pgadmin-login": {"text": "pgAdmin server mode has its own accounts/login.", "components": ["pgadmin"], "sources": ["pgadmin:s2f7e33714435"], "status": "REASONED"}, + "adminer-login": {"text": "Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, + "adminer-file": {"text": "Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy.", "components": ["grafana-pin", "prometheus-pin", "engine-pinned", "moby-pinned"], "sources": ["grafana-pin:s4c6ff3174e7e", "prometheus-pin:s6d5f308b02a2", "engine-pinned:sb3dacfbaf7e1", "engine-pinned:s1c82ff316a7e", "engine-pinned:sc244607b20e4", "moby-pinned:seaf4197aafaa"], "status": "REASONED", "verify": [1]}, + "verify-login": {"text": "Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]}, + "verify-data": {"text": "A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/bi-dashboards.md b/plugin/skills/secure-deployment-config/references/bi-dashboards.md index 3b6ba5094..47e1be786 100644 --- a/plugin/skills/secure-deployment-config/references/bi-dashboards.md +++ b/plugin/skills/secure-deployment-config/references/bi-dashboards.md @@ -1,175 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "72fb83f126c17a7bf6712bea8620686191699a388b522b41778ca65b8b8c506f", - "components": { - "metabase-docs": { - "name": "Metabase docs", - "basis": "unknown", - "sources": { - "sefe12b1b495c": "https://www.metabase.com/docs/latest/", - "s5e2251b3184d": "https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase", - "s521b3063e9f0": "https://www.metabase.com/docs/latest/embedding/public-links", - "s02b7797b6ffe": "https://www.metabase.com/docs/latest/permissions/data", - "s434abdb1aaef": "https://www.metabase.com/docs/latest/databases/users-roles-privileges" - } - }, - "metabase": { - "name": "Metabase", - "basis": "v0.63.18", - "sources": { - "s8df4cbf03036": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L54", - "s7dbf92797642": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L76-L88", - "s7e71ce4cff1b": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/server/instance.clj#L36-L41", - "sed761829c85c": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L79", - "sea59c4ef7f2e": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L164", - "sabe1aa2cf0a9": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L195", - "s18f6f2188fe9": "https://github.com/metabase/metabase/blob/v0.63.18/bin/docker/run_metabase.sh#L2-L5", - "s9b0c73c666ff": "https://github.com/metabase/metabase/blob/v0.63.18/Dockerfile#L66-L69" - } - }, - "environ": { - "name": "environ", - "basis": "1.2.0", - "sources": { - "s37ed6de2d80d": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L32-L43", - "s9754f7c47384": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L64-L79" - } - }, - "ring": { - "name": "Ring Jetty adapter", - "basis": "1.15.3", - "sources": { - "s0455b1b1fb12": "https://github.com/ring-clojure/ring/blob/1.15.3/ring-jetty-adapter/src/ring/adapter/jetty.clj#L202-L207" - } - }, - "jetty": { - "name": "Jetty", - "basis": "12.1.13", - "sources": { - "s8f9e4989175e": "https://github.com/jetty/jetty.project/blob/jetty-12.1.13/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java#L338-L344" - } - }, - "superset": { - "name": "Superset docs", - "basis": "unknown", - "sources": { - "se97be7353585": "https://superset.apache.org/admin-docs/security/" - } - }, - "superset-compose": { - "name": "Superset Compose", - "basis": "3d01094d231712e3470eb625be3f9925aeaeadb5", - "sources": { - "s0ceca7af7f79": "https://github.com/apache/superset/blob/3d01094d231712e3470eb625be3f9925aeaeadb5/docker-compose.yml" - } - }, - "redash-docs": { - "name": "Redash docs", - "basis": "unknown", - "sources": { - "s069a74340b65": "https://redash.io/help/", - "s4c72252b5447": "https://redash.io/help/open-source/setup/", - "sbaf5b44079a0": "https://redash.io/help/open-source/admin-guide/secrets/" - } - }, - "redash": { - "name": "Redash", - "basis": "v26.9.0", - "sources": { - "se93d7c47b037": "https://github.com/getredash/redash/blob/v26.9.0/bin/docker-entrypoint#L49-L50", - "s9f1ef7237804": "https://github.com/getredash/redash/blob/v26.9.0/Dockerfile#L133-L134" - } - }, - "docker": { - "name": "Docker publishing", - "basis": "unknown", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - } - }, - "claims": { - "private": {"text": "Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed.", "components": ["redash-docs", "metabase-docs"], "sources": ["redash-docs:s4c72252b5447", "metabase-docs:s434abdb1aaef"], "status": "REASONED"}, - "metabase-setup": {"text": "First account is admin; finish setup before anyone else can reach it.", "components": ["metabase-docs"], "sources": ["metabase-docs:s5e2251b3184d"], "status": "REASONED"}, - "metabase-precedence": {"text": "Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings.", "components": ["metabase", "environ"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "environ:s37ed6de2d80d", "environ:s9754f7c47384"], "status": "REASONED"}, - "metabase-bind": {"text": "Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy.", "components": ["metabase", "ring", "jetty"], "sources": ["metabase:s7e71ce4cff1b", "metabase:sed761829c85c", "metabase:sea59c4ef7f2e", "metabase:sabe1aa2cf0a9", "ring:s0455b1b1fb12", "jetty:s8f9e4989175e"], "status": "REASONED"}, - "metabase-image": {"text": "Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start.", "components": ["metabase", "docker"], "sources": ["metabase:s18f6f2188fe9", "metabase:s9b0c73c666ff", "docker:s1e53417c513d"], "status": "REASONED"}, - "metabase-port": {"text": "Port defaults 3000 unless the final MB_JETTY_PORT is nonblank.", "components": ["metabase"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "metabase:s7e71ce4cff1b"], "status": "REASONED"}, - "metabase-sharing": {"text": "Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, - "metabase-embed": {"text": "Public embed URLs are recoverable; embeds do not strengthen the public-link boundary.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, - "metabase-sandbox": {"text": "Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries.", "components": ["metabase-docs"], "sources": ["metabase-docs:s02b7797b6ffe"], "status": "REASONED"}, - "superset-secret": {"text": "Set a unique complex random SECRET_KEY before exposure; it signs session cookies.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, - "superset-admin": {"text": "Replace the first-run admin password before exposure.", "components": ["superset", "superset-compose"], "sources": ["superset:se97be7353585", "superset-compose:s0ceca7af7f79"], "status": "REASONED"}, - "superset-public": {"text": "Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, - "superset-copy": {"text": "PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, - "superset-compose": {"text": "Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data.", "components": ["superset-compose"], "sources": ["superset-compose:s0ceca7af7f79"], "status": "REASONED"}, - "redash-setup": {"text": "Create the first-run admin before others reach setup.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, - "redash-https": {"text": "Enforce HTTPS and the private-access pattern.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, - "redash-cookie": {"text": "Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, - "redash-encryption": {"text": "REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, - "redash-bind": {"text": "Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately.", "components": ["redash"], "sources": ["redash:se93d7c47b037", "redash:s9f1ef7237804"], "status": "REASONED"}, - "verify-credentials": {"text": "Confirm no default admin/admin or example credentials work.", "components": ["metabase-docs", "superset-compose", "redash-docs"], "sources": ["metabase-docs:s5e2251b3184d", "superset-compose:s0ceca7af7f79", "redash-docs:s4c72252b5447"], "status": "REASONED"}, - "verify-db": {"text": "Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately.", "components": ["metabase-docs"], "sources": ["metabase-docs:s434abdb1aaef"], "status": "REASONED"}, - "verify-listeners": {"text": "Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation.", "components": ["metabase", "superset-compose", "redash", "docker"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]}, - "verify-ports": {"text": "Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent.", "components": ["metabase", "superset-compose", "redash"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037"], "status": "REASONED", "verify": [1]}, - "verify-data": {"text": "Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive.", "components": ["metabase-docs", "superset", "redash-docs"], "sources": ["metabase-docs:s521b3063e9f0", "superset:se97be7353585", "redash-docs:s4c72252b5447"], "status": "REASONED", "verify": [1]}, - "verify-private": {"text": "For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path.", "components": ["redash-docs", "docker"], "sources": ["redash-docs:s4c72252b5447", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]} - } -} ---- # BI dashboards: Metabase, Superset, Redash - -**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): - -- metabase-docs: Metabase docs (unknown) -- metabase: Metabase (v0.63.18) -- environ: environ (1.2.0) -- ring: Ring Jetty adapter (1.15.3) -- jetty: Jetty (12.1.13) -- superset: Superset docs (unknown) -- superset-compose: Superset Compose (3d01094d231712e3470eb625be3f9925aeaeadb5) -- redash-docs: Redash docs (unknown) -- redash: Redash (v26.9.0) -- docker: Docker publishing (unknown) - -Claims (full source mapping in the front matter): +Written against Metabase v0.63.18, Redash v26.9.0, environ 1.2.0, Ring Jetty adapter 1.15.3, and Jetty 12.1.13; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 25 claims, all reasoned from cited sources. -- private (REASONED; redash-docs, metabase-docs): Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed. -- metabase-setup (REASONED; metabase-docs): First account is admin; finish setup before anyone else can reach it. -- metabase-precedence (REASONED; metabase, environ): Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings. -- metabase-bind (REASONED; metabase, ring, jetty): Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy. -- metabase-image (REASONED; metabase, docker): Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start. -- metabase-port (REASONED; metabase): Port defaults 3000 unless the final MB_JETTY_PORT is nonblank. -- metabase-sharing (REASONED; metabase-docs): Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing. -- metabase-embed (REASONED; metabase-docs): Public embed URLs are recoverable; embeds do not strengthen the public-link boundary. -- metabase-sandbox (REASONED; metabase-docs): Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries. -- superset-secret (REASONED; superset): Set a unique complex random SECRET_KEY before exposure; it signs session cookies. -- superset-admin (REASONED; superset, superset-compose): Replace the first-run admin password before exposure. -- superset-public (REASONED; superset): Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default. -- superset-copy (REASONED; superset): PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards. -- superset-compose (REASONED; superset-compose): Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data. -- redash-setup (REASONED; redash-docs): Create the first-run admin before others reach setup. -- redash-https (REASONED; redash-docs): Enforce HTTPS and the private-access pattern. -- redash-cookie (REASONED; redash-docs): Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it. -- redash-encryption (REASONED; redash-docs): REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control. -- redash-bind (REASONED; redash): Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately. -- verify-credentials (REASONED; metabase-docs, superset-compose, redash-docs): Confirm no default admin/admin or example credentials work. -- verify-db (REASONED; metabase-docs): Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately. -- verify-listeners (REASONED; metabase, superset-compose, redash, docker): Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation. -- verify-ports (REASONED; metabase, superset-compose, redash): Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent. -- verify-data (REASONED; metabase-docs, superset, redash-docs): Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive. -- verify-private (REASONED; redash-docs, docker): For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path. - +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 tools hold live connections to your production databases and cache query results in their own storage. An exposed instance, or one still running default or example credentials, leaks both the @@ -296,3 +129,196 @@ connection string or the tool's own login is not enough. - Redash secret keys (cookie signing, data-source-credential encryption, do not reuse across instances or commit): https://redash.io/help/open-source/admin-guide/secrets/ - Metabase database users, roles, and privileges (a dedicated least-privilege read-only account): https://www.metabase.com/docs/latest/databases/users-roles-privileges - Docker port publishing (a published container port is served through NAT/forwarding rules, so the host may show no matching listener): https://docs.docker.com/engine/network/port-publishing/ + + + +## 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/bi-dashboards.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): + +- metabase: Metabase (v0.63.18) +- redash: Redash (v26.9.0) +- metabase-docs: Metabase docs (unknown) +- superset: Superset docs (unknown) +- superset-compose: Superset Compose (3d01094d231712e3470eb625be3f9925aeaeadb5) +- redash-docs: Redash docs (unknown) +- environ: environ (1.2.0) +- ring: Ring Jetty adapter (1.15.3) +- jetty: Jetty (12.1.13) +- docker: Docker publishing (unknown) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; redash-docs, metabase-docs): Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed. +- metabase-setup (REASONED; metabase-docs): First account is admin; finish setup before anyone else can reach it. +- metabase-precedence (REASONED; metabase, environ): Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings. +- metabase-bind (REASONED; metabase, ring, jetty): Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy. +- metabase-image (REASONED; metabase, docker): Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start. +- metabase-port (REASONED; metabase): Port defaults 3000 unless the final MB_JETTY_PORT is nonblank. +- metabase-sharing (REASONED; metabase-docs): Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing. +- metabase-embed (REASONED; metabase-docs): Public embed URLs are recoverable; embeds do not strengthen the public-link boundary. +- metabase-sandbox (REASONED; metabase-docs): Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries. +- superset-secret (REASONED; superset): Set a unique complex random SECRET_KEY before exposure; it signs session cookies. +- superset-admin (REASONED; superset, superset-compose): Replace the first-run admin password before exposure. +- superset-public (REASONED; superset): Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default. +- superset-copy (REASONED; superset): PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards. +- superset-compose (REASONED; superset-compose): Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data. +- redash-setup (REASONED; redash-docs): Create the first-run admin before others reach setup. +- redash-https (REASONED; redash-docs): Enforce HTTPS and the private-access pattern. +- redash-cookie (REASONED; redash-docs): Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it. +- redash-encryption (REASONED; redash-docs): REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control. +- redash-bind (REASONED; redash): Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately. +- verify-credentials (REASONED; metabase-docs, superset-compose, redash-docs): Confirm no default admin/admin or example credentials work. +- verify-db (REASONED; metabase-docs): Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately. +- verify-listeners (REASONED; metabase, superset-compose, redash, docker): Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation. +- verify-ports (REASONED; metabase, superset-compose, redash): Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent. +- verify-data (REASONED; metabase-docs, superset, redash-docs): Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive. +- verify-private (REASONED; redash-docs, docker): For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "72fb83f126c17a7bf6712bea8620686191699a388b522b41778ca65b8b8c506f", + "components": { + "metabase": { + "name": "Metabase", + "basis": "v0.63.18", + "sources": { + "s8df4cbf03036": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L54", + "s7dbf92797642": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L76-L88", + "s7e71ce4cff1b": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/server/instance.clj#L36-L41", + "sed761829c85c": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L79", + "sea59c4ef7f2e": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L164", + "sabe1aa2cf0a9": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L195", + "s18f6f2188fe9": "https://github.com/metabase/metabase/blob/v0.63.18/bin/docker/run_metabase.sh#L2-L5", + "s9b0c73c666ff": "https://github.com/metabase/metabase/blob/v0.63.18/Dockerfile#L66-L69" + } + }, + "redash": { + "name": "Redash", + "basis": "v26.9.0", + "sources": { + "se93d7c47b037": "https://github.com/getredash/redash/blob/v26.9.0/bin/docker-entrypoint#L49-L50", + "s9f1ef7237804": "https://github.com/getredash/redash/blob/v26.9.0/Dockerfile#L133-L134" + } + }, + "metabase-docs": { + "name": "Metabase docs", + "basis": "unknown", + "sources": { + "sefe12b1b495c": "https://www.metabase.com/docs/latest/", + "s5e2251b3184d": "https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase", + "s521b3063e9f0": "https://www.metabase.com/docs/latest/embedding/public-links", + "s02b7797b6ffe": "https://www.metabase.com/docs/latest/permissions/data", + "s434abdb1aaef": "https://www.metabase.com/docs/latest/databases/users-roles-privileges" + } + }, + "superset": { + "name": "Superset docs", + "basis": "unknown", + "sources": { + "se97be7353585": "https://superset.apache.org/admin-docs/security/" + } + }, + "superset-compose": { + "name": "Superset Compose", + "basis": "3d01094d231712e3470eb625be3f9925aeaeadb5", + "sources": { + "s0ceca7af7f79": "https://github.com/apache/superset/blob/3d01094d231712e3470eb625be3f9925aeaeadb5/docker-compose.yml" + } + }, + "redash-docs": { + "name": "Redash docs", + "basis": "unknown", + "sources": { + "s069a74340b65": "https://redash.io/help/", + "s4c72252b5447": "https://redash.io/help/open-source/setup/", + "sbaf5b44079a0": "https://redash.io/help/open-source/admin-guide/secrets/" + } + }, + "environ": { + "name": "environ", + "basis": "1.2.0", + "sources": { + "s37ed6de2d80d": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L32-L43", + "s9754f7c47384": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L64-L79" + } + }, + "ring": { + "name": "Ring Jetty adapter", + "basis": "1.15.3", + "sources": { + "s0455b1b1fb12": "https://github.com/ring-clojure/ring/blob/1.15.3/ring-jetty-adapter/src/ring/adapter/jetty.clj#L202-L207" + } + }, + "jetty": { + "name": "Jetty", + "basis": "12.1.13", + "sources": { + "s8f9e4989175e": "https://github.com/jetty/jetty.project/blob/jetty-12.1.13/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java#L338-L344" + } + }, + "docker": { + "name": "Docker publishing", + "basis": "unknown", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + } + }, + "claims": { + "private": {"text": "Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed.", "components": ["redash-docs", "metabase-docs"], "sources": ["redash-docs:s4c72252b5447", "metabase-docs:s434abdb1aaef"], "status": "REASONED"}, + "metabase-setup": {"text": "First account is admin; finish setup before anyone else can reach it.", "components": ["metabase-docs"], "sources": ["metabase-docs:s5e2251b3184d"], "status": "REASONED"}, + "metabase-precedence": {"text": "Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings.", "components": ["metabase", "environ"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "environ:s37ed6de2d80d", "environ:s9754f7c47384"], "status": "REASONED"}, + "metabase-bind": {"text": "Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy.", "components": ["metabase", "ring", "jetty"], "sources": ["metabase:s7e71ce4cff1b", "metabase:sed761829c85c", "metabase:sea59c4ef7f2e", "metabase:sabe1aa2cf0a9", "ring:s0455b1b1fb12", "jetty:s8f9e4989175e"], "status": "REASONED"}, + "metabase-image": {"text": "Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start.", "components": ["metabase", "docker"], "sources": ["metabase:s18f6f2188fe9", "metabase:s9b0c73c666ff", "docker:s1e53417c513d"], "status": "REASONED"}, + "metabase-port": {"text": "Port defaults 3000 unless the final MB_JETTY_PORT is nonblank.", "components": ["metabase"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "metabase:s7e71ce4cff1b"], "status": "REASONED"}, + "metabase-sharing": {"text": "Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, + "metabase-embed": {"text": "Public embed URLs are recoverable; embeds do not strengthen the public-link boundary.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, + "metabase-sandbox": {"text": "Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries.", "components": ["metabase-docs"], "sources": ["metabase-docs:s02b7797b6ffe"], "status": "REASONED"}, + "superset-secret": {"text": "Set a unique complex random SECRET_KEY before exposure; it signs session cookies.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-admin": {"text": "Replace the first-run admin password before exposure.", "components": ["superset", "superset-compose"], "sources": ["superset:se97be7353585", "superset-compose:s0ceca7af7f79"], "status": "REASONED"}, + "superset-public": {"text": "Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-copy": {"text": "PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-compose": {"text": "Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data.", "components": ["superset-compose"], "sources": ["superset-compose:s0ceca7af7f79"], "status": "REASONED"}, + "redash-setup": {"text": "Create the first-run admin before others reach setup.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, + "redash-https": {"text": "Enforce HTTPS and the private-access pattern.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, + "redash-cookie": {"text": "Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, + "redash-encryption": {"text": "REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, + "redash-bind": {"text": "Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately.", "components": ["redash"], "sources": ["redash:se93d7c47b037", "redash:s9f1ef7237804"], "status": "REASONED"}, + "verify-credentials": {"text": "Confirm no default admin/admin or example credentials work.", "components": ["metabase-docs", "superset-compose", "redash-docs"], "sources": ["metabase-docs:s5e2251b3184d", "superset-compose:s0ceca7af7f79", "redash-docs:s4c72252b5447"], "status": "REASONED"}, + "verify-db": {"text": "Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately.", "components": ["metabase-docs"], "sources": ["metabase-docs:s434abdb1aaef"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation.", "components": ["metabase", "superset-compose", "redash", "docker"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]}, + "verify-ports": {"text": "Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent.", "components": ["metabase", "superset-compose", "redash"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037"], "status": "REASONED", "verify": [1]}, + "verify-data": {"text": "Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive.", "components": ["metabase-docs", "superset", "redash-docs"], "sources": ["metabase-docs:s521b3063e9f0", "superset:se97be7353585", "redash-docs:s4c72252b5447"], "status": "REASONED", "verify": [1]}, + "verify-private": {"text": "For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path.", "components": ["redash-docs", "docker"], "sources": ["redash-docs:s4c72252b5447", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/deployment-lifecycle.md b/plugin/skills/secure-deployment-config/references/deployment-lifecycle.md index f741e99d7..fbc13453a 100644 --- a/plugin/skills/secure-deployment-config/references/deployment-lifecycle.md +++ b/plugin/skills/secure-deployment-config/references/deployment-lifecycle.md @@ -1,5 +1,235 @@ ---- -version_basis: { +# Deploying safely over time: verify from outside, safe order, previews, teardown + +Written against Nmap documentation (no recorded version), BIND dig documentation 9.18.39, OpenSSL command documentation 3.0, OpenBSD nc manual 7.6, OWASP Web Security Testing Guide v4.2, OAuth2 Proxy v7.15.4, Authelia v4.39.28, and NIST digital identity guidelines SP 800-63B-4, plus 2 more recorded bases; 9 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 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. + +This repository's per-service guides check TLS, binding, and authentication from inside the host and at the moment you set them up. Real exposure is judged from outside the host, and it drifts: a preview URL, a restored backup, or an upgrade can reopen something already closed. This guide is the lifecycle layer around the per-service checks. + +## 1. Verify from outside, not just from the host + +A `curl` or `ss -tlnp` run on the host itself cannot see a firewall the host does not know about, or a platform-level bypass like Docker's iptables rules ([docker.md](docker.md)). Verify from a second network: a phone hotspot, a cloud shell, or a second VM. + +- Port-by-port: `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 REPLACE_WITH_YOUR_PUBLIC_IP "$p"; done`, or a full sweep, `nmap -Pn -p- REPLACE_WITH_YOUR_PUBLIC_IP` (per the nmap reference, which documents `-p-` and `-p 1-65535` as equivalent port-range syntax; this sweeps TCP only, so inventory and separately probe any UDP service with `nmap -sU` on its ports; only scan hosts you own or are authorized to test). Before running either form, use the [guarded Verify block](#verify); for the loop, replace only its IPv4 arm's `nmap -Pn -p- "$1"` with `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 "$1" "$p"; done`, keeping the whole guard and substituting inside the quotes on `set --`. Ubuntu noble netcat-openbsd documents `-v` as verbose output, `-z` as scanning without sending data and `-w` as a connection/idle timeout. BusyBox 1.36.1 small `nc` lacks `-v` and `-z`, and has `-w` only with `NC_EXTRA`; its `NC_110_COMPAT` applet lists `-v` and `-w`, with `-z` conditional on `NC_EXTRA`. CentOS Stream 9 packages Ncat 7.92 as an `nc` alternative; Ncat 7.92's option behaviour is not established here, nor is traditional or GNU netcat behaviour. Check the installed variant. As this guide's assessment, refusal or timeout shows only that this connection failed; an unsupported-option error, local error or silent exit is inconclusive. +- Check every address the service actually has, not just the one you remember configuring: the public IPv4 address, the public IPv6 address if the host has one, and any platform-assigned URL alongside your custom domain (a PaaS default subdomain, per [paas.md](paas.md), often stays reachable even when the custom domain is fronted). A scan of one address that misses the others is not a clean result, it is an incomplete one. +- Cross-reference what is already indexed about your IP with a passive internet-wide scanner such as Shodan or Censys; both build a continuously updated index of internet-connected hosts and services, so a stale exposure can show up there before you find it yourself ([cloud-firewalls.md](cloud-firewalls.md) covers the firewall rules this is checking). + +## 2. Safe initialization order + +This guide advises the following first-deployment order, keeping application access closed until initialization and the fronting gate are ready: + +1. Create the owner or admin account's credentials and any signing secrets (session, JWT, or cookie-signing keys) per [authentication.md](authentication.md); the ordering is this guide's advice, and signing secrets follow that guide's recommendation. +2. Set the registration policy (disable open self-registration, or restrict it to an allowlisted domain) before opening application access, as this guide advises; [authentication.md](authentication.md) covers allowlisting after federated login. +3. Put the fronting gate in place, TLS and any proxy-level authentication, per [free-certificates.md](free-certificates.md) and the fronting-layer guides. For ACME, this guide advises DNS-01 issuance before opening inbound access, or opening only the challenge port during issuance while keeping application access blocked: HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443, whereas DNS-01 needs no inbound port, per the [prerequisite list](free-certificates.md#prerequisites). These challenge requirements do not establish this deployment order. Cloudflare recommends creating the Access application before the tunnel route and warns that without it the published application is available to anyone on the Internet; an already publicly routable origin needs its IP protected by other methods, per the pinned self-hosted application instructions. +4. Only then open application network access, as this guide advises. + +For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin, open to whoever completes it first. [Open WebUI](open-webui.md) at commit 0a7c15832fb30b1903753e83f81dc7d27e5b0944 is one example: its signup handler makes the first account admin; with authentication enabled, ENABLE_SIGNUP and ENABLE_LOGIN_FORM default to true, and an empty database bypasses ENABLE_SIGNUP when the login form is enabled. Once accounts exist, disabled signup refuses registration. These Open WebUI paths do not establish other applications' bootstrap, signing-secret, registration or domain-enforcement behaviour. After initialization, confirm the setup route itself is closed: restart the service and try hitting the initial-setup URL again; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). The Open WebUI signup handler illustrates bootstrap and signup refusal, not every application's setup route or a deployed restart result. + +## 3. Previews and clones get production posture + +A preview deployment or a database clone is not lower stakes just because it is temporary. Vercel's Deployment Protection illustrates the gap: Standard Protection, available on every plan, gates preview and generated deployment URLs but leaves the production domain open by default; the All Deployments scope closes that gap too, and Vercel's September 9, 2026 change made pairing it with Vercel Authentication free on every plan rather than Pro and Enterprise only ([paas.md](paas.md); per Vercel's Deployment Protection changelog of 9 September 2026 and its current Deployment Protection reference, which states that Vercel Authentication for All Deployments does not require a paid add-on). Where the platform's own gate does not cover a hostname, front it the same way as production, for example a Cloudflare Access policy scoped to that preview hostname, per the pinned self-hosted application instructions. Those instructions establish the Access gate, not the platform's coverage of preview URLs. Either way, as this guide's advice, treat a clone's data the same as the original: rotate any credential a clone inherited if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. + +## 4. Teardown: DNS before the app, then revoke the rest + +Retiring a deployment in the wrong order can leave a dangling DNS record pointing at a resource someone else can claim (a subdomain takeover); OWASP's Subdomain Takeover Prevention Cheat Sheet describes this mechanism, and WSTG v4.2 identifies an inactive target and inadequate ownership verification as preconditions. Delete the DNS record (the `CNAME` or `A`/`AAAA`) before you delete or release the underlying app, load balancer, or IP, and wait at least the record's old TTL before decommissioning the resource, following OWASP's order; WSTG supports removing vulnerable records but does not specify that wait. + +RFC 1034 section 3.6 and RFC 1035 section 3.2.1 describe resolver caching and TTL, with TTL bounds clarified by RFC 2181 section 8 and updated by RFC 8767 section 4. **TTL expiry is not a hard bound: RFC 8767 permits serving stale records when they cannot be authoritatively refreshed.** This guide infers that deleting a record does not flush cached answers, so immediately releasing a reclaimable IP or hostname can still send cached clients to its next owner. OWASP describes claimable CNAME targets and released IPs that an attacker obtains after reassignment; the cited files do not establish a particular platform's current reclaimability. This guide advises keeping the resource reserved through the old TTL and until removal is confirmed at every authoritative nameserver, and accounting for serve-stale before release; authoritative removal and elapsed TTL do not prove that every resolver has stopped returning the old mapping. + +Then revoke what pointed at it: access policies (Cloudflare Access or equivalent), API tokens and service credentials scoped to that deployment ([machine-auth.md](machine-auth.md)), and database users created only for it. This remains the guide's retirement requirement: OWASP Secrets Management requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only, and the cited files do not establish removal of an Access application or policy or dropping database users. + +## 5. Re-verify after anything changes + +Rerun the outside-in checks in section 1 and the negative auth tests from [authentication.md](authentication.md) after an upgrade, a backup restore (which, as this guide's assessment, can reintroduce a default account or reset a feature flag), or any change to network policy or auth configuration. Docker's pinned 28.x release notes give concrete change-related cases: 28.0.0 sets IPv6 `FORWARD` policy to `DROP` only when Docker enables forwarding itself, warning that hosts with forwarding already enabled that relied on Docker setting `DROP` may need configuration changes; 28.3.3 fixes direct local-network access to published container ports after a firewalld reload, even when intended for loopback-only access. Open WebUI's pinned configuration supplies an environment-derived signup default and enables persistence by default. As this guide's conditional inference for restores, if database values override the environment after first launch as described in [open-webui.md](open-webui.md), restoring earlier database values could reinstate earlier signup settings; the configuration this guide cites delegates to `models.config.Config`, whose implementation this guide does not cite, so these files do not establish that precedence or general restore behaviour. Nmap supports scanning, not application restore behaviour. Monitor certificate expiry from outside the host too: as this guide's assessment, a renewal cron can silently fail while the host's own view still looks fine, so this guide's advice is an external check (a scheduled `openssl s_client` from another machine, or a third-party uptime/certificate monitor). Certbot v2.3.0's `certbot renew --dry-run` uses the staging test server unless a custom server is set, obtains test certificates without saving them and does not run deploy hooks unless `--run-deploy-hooks` is supplied; applicable deploy hooks then run only after a successful dry run, using the current active certificate. The option documentation also warns that dry runs can temporarily change webserver configuration, reload webservers and run pre/post hooks; the user guide's dry-run example distinguishes it from a subsequent live renewal. From these documented limits, this guide infers that a dry run cannot show whether the last real renewal succeeded; Nmap is outside-check context only. + +## 6. Offboarding + +When a person leaves, revoke access at every layer they touched, not only their password: identity-provider membership (the SSO or directory account), proxy-level sessions (Cloudflare Access or equivalent), application-level sessions on each service, and any personal access tokens or API keys issued to them (this guide's recommendation, as in [authentication.md](authentication.md)). + +Microsoft Entra documents blocking new sign-ins and revoking refresh tokens, but the application must revoke its own session; access-token lifetimes vary, and CAE critical-event enforcement can take up to 15 minutes for supported services. Cloudflare Access documents per-user session revocation, but an active IdP profile can start a new session; after user logout, previously issued tokens stop being accepted in 20-30 seconds. OAuth2 Proxy v7.15.4 documents cookie expiry and refresh, and Authelia v4.39.28 documents cookie expiry and session destruction, overridden by remember-me; these passages do not establish immediate per-user revocation for either proxy. RFC 7009 section 2.1 specifies immediate token invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST SP 800-63B-4 section 4.5 requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. + +Where a system cannot revoke immediately, such as a session cached until its own expiry, measure that system's maximum time-to-revoke and work to shrink it (this guide's recommendation, as in [authentication.md](authentication.md)). The cited files do not establish a provider-membership removal mechanism, a session-revocation mechanism for every application, or this deployment's measured maximum time-to-revoke. For delayed invalidation checks, follow the REASONED offboarding replay check in [authentication.md](authentication.md#quick-checks): matched requests must succeed before revocation, then each old credential must be refused and return no protected data within its configured maximum time-to-revoke; these are test expectations, not observed results. [mfa.md](mfa.md) covers revoking the second factor alongside the account. + +## Common mistakes + +- Verifying only the custom domain and forgetting the platform's own generated URL, which is often still reachable and unauthenticated. +- Deleting the app first and the DNS record later, leaving the dangling-CNAME window OWASP warns about when the target can be claimed; waiting only for TTL expiry also misses the serve-stale exception in RFC 8767. +- Trusting a local `certbot renew --dry-run` as proof that production certificates are actually renewing; Certbot v2.3.0 documents test certificates without saving them, staging unless a custom server is set, and deploy hooks only with `--run-deploy-hooks` after a successful dry run, using the active certificate. This guide infers that this test does not establish whether the last real renewal succeeded. + +## Verify + +REASONED: outside-network scans, DNS retirement and certificate inspection; no deployment, authoritative DNS zone or second-network probe host was supplied for this metadata review, and no run is recorded. The commands and expected readings below follow the cited Nmap, dig and OpenSSL 3.0 documentation and the guide's lifecycle reasoning; Sources has no curl reference. DNS response interpretations follow RFC 2308 section 2, and cache timing follows the cited DNS RFCs, including RFC 8767's serve-stale exception; the authoritative-removal checks are this guide's advice and do not establish that all recursive caches have refreshed. + +```bash +# From a second network, not the host itself: +( # a subshell, so your own script arguments are untouched + set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IP' # replace inside the quotes, keeping them + [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } + shift + [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } + case "$1" in + *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; + *) nmap -Pn -p- "$1" ;; # TCP only: only the intended ports answer here; probe any UDP service separately (nmap -sU) + # and the scan actually ran: nmap reporting a host down, a permission error, or no output at all is + # inconclusive, not a clean result + esac +) +( # a subshell, so your own script arguments are untouched + set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IPV6' # replace inside the quotes, keeping them + [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } + shift + [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } + case "$1" in + *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; + *) nmap -Pn -6 -p- "$1" ;; # same, over the public IPv6 address (TCP only) + # same reading: a scan that did not run is not a clean scan + esac +) +# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv +curl -q -sI https://retired-preview.example.com/ # a connection/DNS/TLS failure here is NOT proof the record is gone (a dangling record to a released target fails the same way); use the dig check below, and confirm at the authoritative nameservers, to establish removal +# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv +for t in A AAAA CNAME; do echo "$t:"; dig +short retired-preview.example.com "$t"; done # checks your configured resolver only, so it is not authoritative: any A/AAAA/CNAME answer means that resolver still returns a mapping (investigate before release; an answer alone does not prove the target is dangling), and empty output is inconclusive. Confirm removal against each authoritative nameserver directly (dig @AUTH_NS name TYPE +norecurse), requiring NXDOMAIN or NOERROR/NODATA with no CNAME; treat SERVFAIL/referral/timeout as inconclusive +# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv +openssl s_client -connect app.example.com:443 -servername app.example.com \ + -verify_hostname app.example.com -verify_return_error +- Certbot dry-run test-server dispatch, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L210-L211 +- Certbot staging-server selection and custom-server exception, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L237-L264 +- Certbot user-guide dry-run and subsequent live-renewal example, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L786-L798 + + + +## 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/deployment-lifecycle.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): + +- nmap: Nmap documentation (unknown) +- shodan: Shodan (unknown) +- censys: Censys (unknown) +- vercel: Vercel Deployment Protection (unknown) +- access: Cloudflare Access policies (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- dns: BIND dig documentation (9.18.39) +- openssl: OpenSSL command documentation (3.0) +- openbsd-nc: OpenBSD nc manual (7.6) +- owasp-takeover: OWASP subdomain takeover prevention (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) +- wstg: OWASP Web Security Testing Guide (v4.2) +- dns-rfcs: DNS caching RFCs (unknown) +- owasp-docs: OWASP security guidance (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) +- entra-docs: Microsoft Entra revocation model (231747abc59ae3d50c74e215c1cdd592eb980723) +- cloudflare-access: Cloudflare Access and origin configuration (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- oauth2-proxy: OAuth2 Proxy (v7.15.4) +- authelia: Authelia (v4.39.28) +- oauth-revocation: OAuth 2.0 Token Revocation RFC (unknown) +- nist: NIST digital identity guidelines (SP 800-63B-4) +- ubuntu-nc: Ubuntu noble netcat-openbsd source (c047f1a4572e796c8643fb7e5a719a9d960a677b) +- c9s-nmap: CentOS Stream 9 nmap packaging (17c2b7795d821cd7fbe311cf3f885be3ea1f122f) +- busybox: BusyBox 1.36.1 (1_36_1) +- docker-notes: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) +- webui: Open WebUI (0a7c15832fb30b1903753e83f81dc7d27e5b0944) +- certbot-v230: Certbot user guide and source (v2.3.0) +- le: Let's Encrypt documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- outside (REASONED; nmap): Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour. +- tcp-scan (REASONED; nmap, openbsd-nc, ubuntu-nc, c9s-nmap, busybox): Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive. +- udp-scan (REASONED; nmap): The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU. +- all-addresses (REASONED; nmap, vercel): Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others. +- passive-index (REASONED; shodan, censys): Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan. +- initialize-secrets (REASONED; access, webui): As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here. +- registration (REASONED; access, webui): As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here. +- fronting-order (REASONED; access, le): As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice. +- setup-route (REASONED; access, webui): After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only. +- preview-standard (REASONED; vercel): Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open. +- preview-all (REASONED; vercel): All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on. +- preview-gate (REASONED; access, vercel): Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs. +- clone-secrets (REASONED; access): As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. +- dns-retirement (REASONED; dns, owasp-takeover, wstg, dns-rfcs): Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection. +- retire-credentials (REASONED; access, owasp-docs): After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users. +- reverify (REASONED; nmap, docker-notes, webui): Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only. +- certificate-monitor (REASONED; nmap, openssl, certbot-v230): Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only. +- offboarding (REASONED; access, owasp-docs, entra-docs, cloudflare-access, oauth2-proxy, authelia, oauth-revocation, nist): When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations. +- verify-ipv4 (REASONED; nmap): From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive. +- verify-ipv6 (REASONED; nmap): Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive. +- verify-dns (REASONED; dns, dns-rfcs): curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive. +- verify-certificate (REASONED; nmap, openssl): The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only. + +````json +{ "schema": 1, "checked": "2026-10-18", "documentation_checked": "2026-09", @@ -231,209 +461,5 @@ version_basis: { "verify-certificate": {"text": "The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only.", "components": ["nmap", "openssl"], "sources": ["nmap:sf8404f695ccd", "openssl:s00aaf106164f", "openssl:s0856f4a58574"], "status": "REASONED", "verify": [1]} } } ---- -# Deploying safely over time: verify from outside, safe order, previews, teardown - - -**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): - -- nmap: Nmap documentation (unknown) -- shodan: Shodan (unknown) -- censys: Censys (unknown) -- vercel: Vercel Deployment Protection (unknown) -- access: Cloudflare Access policies (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- dns: BIND dig documentation (9.18.39) -- openssl: OpenSSL command documentation (3.0) -- openbsd-nc: OpenBSD nc manual (7.6) -- owasp-takeover: OWASP subdomain takeover prevention (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) -- wstg: OWASP Web Security Testing Guide (v4.2) -- dns-rfcs: DNS caching RFCs (unknown) -- owasp-docs: OWASP security guidance (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) -- entra-docs: Microsoft Entra revocation model (231747abc59ae3d50c74e215c1cdd592eb980723) -- cloudflare-access: Cloudflare Access and origin configuration (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- oauth2-proxy: OAuth2 Proxy (v7.15.4) -- authelia: Authelia (v4.39.28) -- oauth-revocation: OAuth 2.0 Token Revocation RFC (unknown) -- nist: NIST digital identity guidelines (SP 800-63B-4) -- ubuntu-nc: Ubuntu noble netcat-openbsd source (c047f1a4572e796c8643fb7e5a719a9d960a677b) -- c9s-nmap: CentOS Stream 9 nmap packaging (17c2b7795d821cd7fbe311cf3f885be3ea1f122f) -- busybox: BusyBox 1.36.1 (1_36_1) -- docker-notes: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) -- webui: Open WebUI (0a7c15832fb30b1903753e83f81dc7d27e5b0944) -- certbot-v230: Certbot user guide and source (v2.3.0) -- le: Let's Encrypt documentation (unknown) - -Claims (full source mapping in the front matter): - -- outside (REASONED; nmap): Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour. -- tcp-scan (REASONED; nmap, openbsd-nc, ubuntu-nc, c9s-nmap, busybox): Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive. -- udp-scan (REASONED; nmap): The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU. -- all-addresses (REASONED; nmap, vercel): Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others. -- passive-index (REASONED; shodan, censys): Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan. -- initialize-secrets (REASONED; access, webui): As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here. -- registration (REASONED; access, webui): As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here. -- fronting-order (REASONED; access, le): As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice. -- setup-route (REASONED; access, webui): After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only. -- preview-standard (REASONED; vercel): Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open. -- preview-all (REASONED; vercel): All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on. -- preview-gate (REASONED; access, vercel): Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs. -- clone-secrets (REASONED; access): As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. -- dns-retirement (REASONED; dns, owasp-takeover, wstg, dns-rfcs): Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection. -- retire-credentials (REASONED; access, owasp-docs): After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users. -- reverify (REASONED; nmap, docker-notes, webui): Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only. -- certificate-monitor (REASONED; nmap, openssl, certbot-v230): Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only. -- offboarding (REASONED; access, owasp-docs, entra-docs, cloudflare-access, oauth2-proxy, authelia, oauth-revocation, nist): When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations. -- verify-ipv4 (REASONED; nmap): From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive. -- verify-ipv6 (REASONED; nmap): Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive. -- verify-dns (REASONED; dns, dns-rfcs): curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive. -- verify-certificate (REASONED; nmap, openssl): The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only. +```` - -This repository's per-service guides check TLS, binding, and authentication from inside the host and at the moment you set them up. Real exposure is judged from outside the host, and it drifts: a preview URL, a restored backup, or an upgrade can reopen something already closed. This guide is the lifecycle layer around the per-service checks. - -## 1. Verify from outside, not just from the host - -A `curl` or `ss -tlnp` run on the host itself cannot see a firewall the host does not know about, or a platform-level bypass like Docker's iptables rules ([docker.md](docker.md)). Verify from a second network: a phone hotspot, a cloud shell, or a second VM. - -- Port-by-port: `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 REPLACE_WITH_YOUR_PUBLIC_IP "$p"; done`, or a full sweep, `nmap -Pn -p- REPLACE_WITH_YOUR_PUBLIC_IP` (per the nmap reference, which documents `-p-` and `-p 1-65535` as equivalent port-range syntax; this sweeps TCP only, so inventory and separately probe any UDP service with `nmap -sU` on its ports; only scan hosts you own or are authorized to test). Before running either form, use the [guarded Verify block](#verify); for the loop, replace only its IPv4 arm's `nmap -Pn -p- "$1"` with `for p in 22 80 443 3000 5432 6379 27017; do nc -vz -w 3 "$1" "$p"; done`, keeping the whole guard and substituting inside the quotes on `set --`. Ubuntu noble netcat-openbsd documents `-v` as verbose output, `-z` as scanning without sending data and `-w` as a connection/idle timeout. BusyBox 1.36.1 small `nc` lacks `-v` and `-z`, and has `-w` only with `NC_EXTRA`; its `NC_110_COMPAT` applet lists `-v` and `-w`, with `-z` conditional on `NC_EXTRA`. CentOS Stream 9 packages Ncat 7.92 as an `nc` alternative; Ncat 7.92's option behaviour is not established here, nor is traditional or GNU netcat behaviour. Check the installed variant. As this guide's assessment, refusal or timeout shows only that this connection failed; an unsupported-option error, local error or silent exit is inconclusive. -- Check every address the service actually has, not just the one you remember configuring: the public IPv4 address, the public IPv6 address if the host has one, and any platform-assigned URL alongside your custom domain (a PaaS default subdomain, per [paas.md](paas.md), often stays reachable even when the custom domain is fronted). A scan of one address that misses the others is not a clean result, it is an incomplete one. -- Cross-reference what is already indexed about your IP with a passive internet-wide scanner such as Shodan or Censys; both build a continuously updated index of internet-connected hosts and services, so a stale exposure can show up there before you find it yourself ([cloud-firewalls.md](cloud-firewalls.md) covers the firewall rules this is checking). - -## 2. Safe initialization order - -This guide advises the following first-deployment order, keeping application access closed until initialization and the fronting gate are ready: - -1. Create the owner or admin account's credentials and any signing secrets (session, JWT, or cookie-signing keys) per [authentication.md](authentication.md); the ordering is this guide's advice, and signing secrets follow that guide's recommendation. -2. Set the registration policy (disable open self-registration, or restrict it to an allowlisted domain) before opening application access, as this guide advises; [authentication.md](authentication.md) covers allowlisting after federated login. -3. Put the fronting gate in place, TLS and any proxy-level authentication, per [free-certificates.md](free-certificates.md) and the fronting-layer guides. For ACME, this guide advises DNS-01 issuance before opening inbound access, or opening only the challenge port during issuance while keeping application access blocked: HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443, whereas DNS-01 needs no inbound port, per the [prerequisite list](free-certificates.md#prerequisites). These challenge requirements do not establish this deployment order. Cloudflare recommends creating the Access application before the tunnel route and warns that without it the published application is available to anyone on the Internet; an already publicly routable origin needs its IP protected by other methods, per the pinned self-hosted application instructions. -4. Only then open application network access, as this guide advises. - -For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin, open to whoever completes it first. [Open WebUI](open-webui.md) at commit 0a7c15832fb30b1903753e83f81dc7d27e5b0944 is one example: its signup handler makes the first account admin; with authentication enabled, ENABLE_SIGNUP and ENABLE_LOGIN_FORM default to true, and an empty database bypasses ENABLE_SIGNUP when the login form is enabled. Once accounts exist, disabled signup refuses registration. These Open WebUI paths do not establish other applications' bootstrap, signing-secret, registration or domain-enforcement behaviour. After initialization, confirm the setup route itself is closed: restart the service and try hitting the initial-setup URL again; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). The Open WebUI signup handler illustrates bootstrap and signup refusal, not every application's setup route or a deployed restart result. - -## 3. Previews and clones get production posture - -A preview deployment or a database clone is not lower stakes just because it is temporary. Vercel's Deployment Protection illustrates the gap: Standard Protection, available on every plan, gates preview and generated deployment URLs but leaves the production domain open by default; the All Deployments scope closes that gap too, and Vercel's September 9, 2026 change made pairing it with Vercel Authentication free on every plan rather than Pro and Enterprise only ([paas.md](paas.md); per Vercel's Deployment Protection changelog of 9 September 2026 and its current Deployment Protection reference, which states that Vercel Authentication for All Deployments does not require a paid add-on). Where the platform's own gate does not cover a hostname, front it the same way as production, for example a Cloudflare Access policy scoped to that preview hostname, per the pinned self-hosted application instructions. Those instructions establish the Access gate, not the platform's coverage of preview URLs. Either way, as this guide's advice, treat a clone's data the same as the original: rotate any credential a clone inherited if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. - -## 4. Teardown: DNS before the app, then revoke the rest - -Retiring a deployment in the wrong order can leave a dangling DNS record pointing at a resource someone else can claim (a subdomain takeover); OWASP's Subdomain Takeover Prevention Cheat Sheet describes this mechanism, and WSTG v4.2 identifies an inactive target and inadequate ownership verification as preconditions. Delete the DNS record (the `CNAME` or `A`/`AAAA`) before you delete or release the underlying app, load balancer, or IP, and wait at least the record's old TTL before decommissioning the resource, following OWASP's order; WSTG supports removing vulnerable records but does not specify that wait. - -RFC 1034 section 3.6 and RFC 1035 section 3.2.1 describe resolver caching and TTL, with TTL bounds clarified by RFC 2181 section 8 and updated by RFC 8767 section 4. **TTL expiry is not a hard bound: RFC 8767 permits serving stale records when they cannot be authoritatively refreshed.** This guide infers that deleting a record does not flush cached answers, so immediately releasing a reclaimable IP or hostname can still send cached clients to its next owner. OWASP describes claimable CNAME targets and released IPs that an attacker obtains after reassignment; the cited files do not establish a particular platform's current reclaimability. This guide advises keeping the resource reserved through the old TTL and until removal is confirmed at every authoritative nameserver, and accounting for serve-stale before release; authoritative removal and elapsed TTL do not prove that every resolver has stopped returning the old mapping. - -Then revoke what pointed at it: access policies (Cloudflare Access or equivalent), API tokens and service credentials scoped to that deployment ([machine-auth.md](machine-auth.md)), and database users created only for it. This remains the guide's retirement requirement: OWASP Secrets Management requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only, and the cited files do not establish removal of an Access application or policy or dropping database users. - -## 5. Re-verify after anything changes - -Rerun the outside-in checks in section 1 and the negative auth tests from [authentication.md](authentication.md) after an upgrade, a backup restore (which, as this guide's assessment, can reintroduce a default account or reset a feature flag), or any change to network policy or auth configuration. Docker's pinned 28.x release notes give concrete change-related cases: 28.0.0 sets IPv6 `FORWARD` policy to `DROP` only when Docker enables forwarding itself, warning that hosts with forwarding already enabled that relied on Docker setting `DROP` may need configuration changes; 28.3.3 fixes direct local-network access to published container ports after a firewalld reload, even when intended for loopback-only access. Open WebUI's pinned configuration supplies an environment-derived signup default and enables persistence by default. As this guide's conditional inference for restores, if database values override the environment after first launch as described in [open-webui.md](open-webui.md), restoring earlier database values could reinstate earlier signup settings; the configuration this guide cites delegates to `models.config.Config`, whose implementation this guide does not cite, so these files do not establish that precedence or general restore behaviour. Nmap supports scanning, not application restore behaviour. Monitor certificate expiry from outside the host too: as this guide's assessment, a renewal cron can silently fail while the host's own view still looks fine, so this guide's advice is an external check (a scheduled `openssl s_client` from another machine, or a third-party uptime/certificate monitor). Certbot v2.3.0's `certbot renew --dry-run` uses the staging test server unless a custom server is set, obtains test certificates without saving them and does not run deploy hooks unless `--run-deploy-hooks` is supplied; applicable deploy hooks then run only after a successful dry run, using the current active certificate. The option documentation also warns that dry runs can temporarily change webserver configuration, reload webservers and run pre/post hooks; the user guide's dry-run example distinguishes it from a subsequent live renewal. From these documented limits, this guide infers that a dry run cannot show whether the last real renewal succeeded; Nmap is outside-check context only. - -## 6. Offboarding - -When a person leaves, revoke access at every layer they touched, not only their password: identity-provider membership (the SSO or directory account), proxy-level sessions (Cloudflare Access or equivalent), application-level sessions on each service, and any personal access tokens or API keys issued to them (this guide's recommendation, as in [authentication.md](authentication.md)). - -Microsoft Entra documents blocking new sign-ins and revoking refresh tokens, but the application must revoke its own session; access-token lifetimes vary, and CAE critical-event enforcement can take up to 15 minutes for supported services. Cloudflare Access documents per-user session revocation, but an active IdP profile can start a new session; after user logout, previously issued tokens stop being accepted in 20-30 seconds. OAuth2 Proxy v7.15.4 documents cookie expiry and refresh, and Authelia v4.39.28 documents cookie expiry and session destruction, overridden by remember-me; these passages do not establish immediate per-user revocation for either proxy. RFC 7009 section 2.1 specifies immediate token invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST SP 800-63B-4 section 4.5 requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. - -Where a system cannot revoke immediately, such as a session cached until its own expiry, measure that system's maximum time-to-revoke and work to shrink it (this guide's recommendation, as in [authentication.md](authentication.md)). The cited files do not establish a provider-membership removal mechanism, a session-revocation mechanism for every application, or this deployment's measured maximum time-to-revoke. For delayed invalidation checks, follow the REASONED offboarding replay check in [authentication.md](authentication.md#quick-checks): matched requests must succeed before revocation, then each old credential must be refused and return no protected data within its configured maximum time-to-revoke; these are test expectations, not observed results. [mfa.md](mfa.md) covers revoking the second factor alongside the account. - -## Common mistakes - -- Verifying only the custom domain and forgetting the platform's own generated URL, which is often still reachable and unauthenticated. -- Deleting the app first and the DNS record later, leaving the dangling-CNAME window OWASP warns about when the target can be claimed; waiting only for TTL expiry also misses the serve-stale exception in RFC 8767. -- Trusting a local `certbot renew --dry-run` as proof that production certificates are actually renewing; Certbot v2.3.0 documents test certificates without saving them, staging unless a custom server is set, and deploy hooks only with `--run-deploy-hooks` after a successful dry run, using the active certificate. This guide infers that this test does not establish whether the last real renewal succeeded. - -## Verify - -REASONED: outside-network scans, DNS retirement and certificate inspection; no deployment, authoritative DNS zone or second-network probe host was supplied for this metadata review, and no run is recorded. The commands and expected readings below follow the cited Nmap, dig and OpenSSL 3.0 documentation and the guide's lifecycle reasoning; Sources has no curl reference. DNS response interpretations follow RFC 2308 section 2, and cache timing follows the cited DNS RFCs, including RFC 8767's serve-stale exception; the authoritative-removal checks are this guide's advice and do not establish that all recursive caches have refreshed. - -```bash -# From a second network, not the host itself: -( # a subshell, so your own script arguments are untouched - set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IP' # replace inside the quotes, keeping them - [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } - shift - [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } - case "$1" in - *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; - *) nmap -Pn -p- "$1" ;; # TCP only: only the intended ports answer here; probe any UDP service separately (nmap -sU) - # and the scan actually ran: nmap reporting a host down, a permission error, or no output at all is - # inconclusive, not a clean result - esac -) -( # a subshell, so your own script arguments are untouched - set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_YOUR_PUBLIC_IPV6' # replace inside the quotes, keeping them - [ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo "paste the whole block, including its set -- line; not probing"; exit; } - shift - [ "$#" -eq 1 ] || { echo "the set -- line needs exactly 1 value; not probing"; exit; } - case "$1" in - *REPLACE_WITH_*|*YOUR_PUBLIC_IP*|"") echo "substitute your own address on the set -- line above; not probing" ;; - *) nmap -Pn -6 -p- "$1" ;; # same, over the public IPv6 address (TCP only) - # same reading: a scan that did not run is not a clean scan - esac -) -# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv -curl -q -sI https://retired-preview.example.com/ # a connection/DNS/TLS failure here is NOT proof the record is gone (a dangling record to a released target fails the same way); use the dig check below, and confirm at the authoritative nameservers, to establish removal -# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv -for t in A AAAA CNAME; do echo "$t:"; dig +short retired-preview.example.com "$t"; done # checks your configured resolver only, so it is not authoritative: any A/AAAA/CNAME answer means that resolver still returns a mapping (investigate before release; an answer alone does not prove the target is dangling), and empty output is inconclusive. Confirm removal against each authoritative nameserver directly (dig @AUTH_NS name TYPE +norecurse), requiring NXDOMAIN or NOERROR/NODATA with no CNAME; treat SERVFAIL/referral/timeout as inconclusive -# guard-conventions: allow probe of an illustrative example host; no reader-substituted placeholder in this probe's argv -openssl s_client -connect app.example.com:443 -servername app.example.com \ - -verify_hostname app.example.com -verify_return_error -- Certbot dry-run test-server dispatch, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L210-L211 -- Certbot staging-server selection and custom-server exception, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L237-L264 -- Certbot user-guide dry-run and subsequent live-renewal example, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L786-L798 diff --git a/plugin/skills/secure-deployment-config/references/devops-uis.md b/plugin/skills/secure-deployment-config/references/devops-uis.md index ea2312918..dd060d81d 100644 --- a/plugin/skills/secure-deployment-config/references/devops-uis.md +++ b/plugin/skills/secure-deployment-config/references/devops-uis.md @@ -1,343 +1,12 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "7cea8ce7dc2689ed90673f4d926b8259c0c8f0b24281b47ea184dfe91ffa1289", - "components": { - "portainer": { - "name": "Portainer", - "basis": "unknown", - "sources": { - "s3e163c7d3c38": "https://docs.portainer.io/start/install-ce/server/docker/linux", - "s115489d2f86f": "https://docs.portainer.io/start/install-ce/server/setup", - "s59297757a042": "https://docs.portainer.io/admin/settings/authentication", - "s8901bbe7c3d3": "https://docs.portainer.io/admin/settings/authentication/oauth" - } - }, - "coolify": { - "name": "Coolify", - "basis": "unknown", - "sources": { - "s41e2d21202d9": "https://coolify.io/docs/start-with-self-hosted", - "sf2ae62425b35": "https://coolify.io/docs/core/infrastructure/servers/firewall", - "sf5d819d5d87d": "https://coolify.io/docs/core/networking/proxy/overview", - "s90addf9d8d5d": "https://coolify.io/docs/core/networking/dns", - "s683244a61c48": "https://coolify.io/docs/core/infrastructure/servers/openssh" - } - }, - "dokploy": { - "name": "Dokploy", - "basis": "unknown", - "sources": { - "sc2cc854c9b7e": "https://docs.dokploy.com/docs/core/installation" - } - }, - "npm": { - "name": "Nginx Proxy Manager", - "basis": "unknown", - "sources": { - "se553d384e783": "https://nginxproxymanager.com/setup/" - } - }, - "vaultwarden": { - "name": "Vaultwarden docs", - "basis": "unknown", - "sources": { - "s0bbe8ad856cd": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page", - "s6d3d0d02dcc9": "https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users", - "s60d064cae50e": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS" - } - }, - "vaultwarden-pin": { - "name": "Vaultwarden config", - "basis": "3347698712d3e99e652ad4394ef2fc0ce2800fdd", - "sources": { - "s31c5fc79f5a0": "https://github.com/dani-garcia/vaultwarden/blob/3347698712d3e99e652ad4394ef2fc0ce2800fdd/.env.template" - } - }, - "dashboard": { - "name": "Dashboard docs", - "basis": "unknown", - "sources": { - "s73a36a43f6b5": "https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/" - } - }, - "dashboard-old": { - "name": "Dashboard arguments", - "basis": "v2.7.0", - "sources": { - "sacea2944651e": "https://github.com/kubernetes-retired/dashboard/blob/v2.7.0/docs/common/dashboard-arguments.md" - } - }, - "jenkins": { - "name": "Jenkins", - "basis": "unknown", - "sources": { - "s524c8d5871f1": "https://www.jenkins.io/doc/book/security/managing-security/", - "sdfee751aeeee": "https://www.jenkins.io/doc/book/security/access-control/", - "s6f5287fd36d4": "https://www.jenkins.io/doc/book/security/csrf-protection/" - } - }, - "gitea": { - "name": "Gitea docs", - "basis": "unknown", - "sources": { - "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet", - "s3b7060d58383": "https://docs.gitea.com/usage/user-setting/multi-factor-authentication/" - } - }, - "gitlab": { - "name": "GitLab CE", - "basis": "unknown", - "sources": { - "s3b7ac561a92e": "https://docs.gitlab.com/install/docker/installation/", - "s6e8d16cad2ef": "https://docs.gitlab.com/administration/settings/sign_up_restrictions/", - "s6b8a68a77e17": "https://docs.gitlab.com/administration/settings/visibility_and_access_controls/", - "s0347014ce554": "https://docs.gitlab.com/security/two_factor_authentication/" - } - }, - "kuma": { - "name": "Uptime Kuma", - "basis": "unknown", - "sources": { - "s05b853ba300b": "https://github.com/louislam/uptime-kuma", - "sed52bfca4039": "https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" - } - }, - "docker": { - "name": "Docker Engine", - "basis": "unknown", - "sources": { - "sfe15a8156c18": "https://docs.docker.com/engine/security/protect-access/", - "s1b5ec54421b0": "https://docs.docker.com/engine/daemon/remote-access/" - } - }, - "dozzle": { - "name": "Dozzle", - "basis": "unknown", - "sources": { - "s45b3eab2515b": "https://dozzle.dev/guide/authentication" - } - }, - "filebrowser": { - "name": "Filebrowser", - "basis": "unknown", - "sources": { - "s6386c9e2356b": "https://github.com/filebrowser/filebrowser" - } - }, - "nodered": { - "name": "Node-RED", - "basis": "unknown", - "sources": { - "sa94f329023fe": "https://nodered.org/docs/user-guide/runtime/securing-node-red" - } - }, - "gitea-pin": { - "name": "Gitea listener", - "basis": "v1.27.3", - "sources": { - "s31dd622b1c12": "https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122" - } - }, - "gitea-mfa-min": { - "name": "Gitea MFA minimum", - "basis": "1.24", - "sources": { - "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet" - } - }, - "registry-docs": { - "name": "Docker Registry documentation (rolling)", - "basis": "unknown", - "sources": { - "sa315b3f8bfa6": "https://distribution.github.io/distribution/about/deploying/" - } - }, - "htpasswd": { - "name": "Apache htpasswd", - "basis": "2.4", - "sources": { - "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" - } - }, - "owasp-passwords": { - "name": "OWASP password storage", - "basis": "1a3f58ed2714f258a1d713966457f1a0f426e841", - "sources": { - "s0f70ec4aa271": "https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139" - } - } - }, - "claims": { - "private": {"text": "Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, - "portainer-ports": {"text": "UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute.", "components": ["portainer"], "sources": ["portainer:s3e163c7d3c38"], "status": "REASONED"}, - "portainer-setup": {"text": "Setup requires the logged setup_token; first user is admin with a password of at least 12 characters.", "components": ["portainer"], "sources": ["portainer:s115489d2f86f"], "status": "REASONED"}, - "portainer-auth": {"text": "Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider.", "components": ["portainer"], "sources": ["portainer:s59297757a042", "portainer:s8901bbe7c3d3"], "status": "REASONED"}, - "portainer-socket": {"text": "Mounted docker.sock grants host-root power; treat a Portainer admin accordingly.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, - "coolify-setup": {"text": "HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately.", "components": ["coolify"], "sources": ["coolify:s41e2d21202d9"], "status": "REASONED"}, - "coolify-tls": {"text": "Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35", "coolify:sf5d819d5d87d", "coolify:s90addf9d8d5d"], "status": "REASONED"}, - "coolify-firewall": {"text": "Docker bypasses UFW; restrict dashboard ports at the cloud firewall.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35"], "status": "REASONED"}, - "coolify-ssh": {"text": "Server SSH keys require no passphrase; stored key material is the whole secret.", "components": ["coolify"], "sources": ["coolify:s683244a61c48"], "status": "REASONED"}, - "dokploy-ports": {"text": "UI is 3000, Traefik 80/443; first setup creates admin.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, - "dokploy-tls": {"text": "Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, - "npm-ports": {"text": "Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, - "npm-admin": {"text": "Change default first-run admin credentials before other work.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, - "vaultwarden-tls": {"text": "Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s60d064cae50e", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-signup": {"text": "SIGNUPS_ALLOWED defaults true; set false to close registration.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-invites": {"text": "INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-admin": {"text": "ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-session": {"text": "Admin sessions expire after 20 minutes by default.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "vaultwarden-port": {"text": "Container port 80 or non-Docker 8000; publish only to loopback/proxy network.", "components": ["vaultwarden-pin"], "sources": ["vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, - "dashboard-lifecycle": {"text": "Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "dashboard-forward": {"text": "Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "dashboard-token": {"text": "Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "dashboard-old-flags": {"text": "Older 2.x enable-skip-login and enable-insecure-login default false; keep both false.", "components": ["dashboard-old"], "sources": ["dashboard-old:sacea2944651e"], "status": "REASONED"}, - "dashboard-mfa": {"text": "API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, - "jenkins-auth": {"text": "Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, - "jenkins-matrix": {"text": "Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control.", "components": ["jenkins"], "sources": ["jenkins:sdfee751aeeee"], "status": "REASONED"}, - "jenkins-csrf": {"text": "Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch.", "components": ["jenkins"], "sources": ["jenkins:s6f5287fd36d4"], "status": "REASONED"}, - "jenkins-agents": {"text": "Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1"], "status": "REASONED"}, - "jenkins-mfa": {"text": "No native second factor; enforce MFA through SSO/fronting identity.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, - "gitea-bind": {"text": "HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy.", "components": ["gitea-pin"], "sources": ["gitea-pin:s31dd622b1c12"], "status": "REASONED"}, - "gitea-tls": {"text": "Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, - "gitea-registration": {"text": "Set service DISABLE_REGISTRATION=true; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, - "gitea-private": {"text": "Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, - "gitea-mfa": {"text": "Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later.", "components": ["gitea", "gitea-mfa-min"], "sources": ["gitea:s57933519c308", "gitea:s3b7060d58383", "gitea-mfa-min:s57933519c308"], "status": "REASONED"}, - "gitea-tokens": {"text": "With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens.", "components": ["gitea"], "sources": ["gitea:s3b7060d58383"], "status": "REASONED"}, - "gitlab-root": {"text": "Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED"}, - "gitlab-signup": {"text": "signup_enabled defaults true; disable for a private forge.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, - "gitlab-approval": {"text": "New instances require admin approval after signup by default; do not rely on approval alone.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, - "gitlab-visibility": {"text": "Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants.", "components": ["gitlab"], "sources": ["gitlab:s6b8a68a77e17"], "status": "REASONED"}, - "gitlab-mfa": {"text": "Enforce two-factor authentication for every user; available on all tiers.", "components": ["gitlab"], "sources": ["gitlab:s0347014ce554"], "status": "REASONED"}, - "kuma-proxy": {"text": "Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b", "kuma:sed52bfca4039"], "status": "REASONED"}, - "kuma-admin": {"text": "Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b"], "status": "REASONED"}, - "docker-root": {"text": "Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375.", "components": ["docker"], "sources": ["docker:sfe15a8156c18", "docker:s1b5ec54421b0"], "status": "REASONED"}, - "docker-ssh": {"text": "DOCKER_HOST/context can select SSH to the Unix socket without a new listener.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, - "docker-mtls": {"text": "2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, - "dozzle-socket": {"text": "Docker-socket access has host-level power; keep Dozzle private with fronting MFA.", "components": ["dozzle", "docker"], "sources": ["dozzle:s45b3eab2515b", "docker:sfe15a8156c18"], "status": "REASONED"}, - "dozzle-auth": {"text": "Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, - "dozzle-actions": {"text": "Leave optional container actions/shell off unless needed; either enables remote command execution.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, - "registry-auth": {"text": "registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, - "registry-bcrypt": {"text": "Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ.", "components": ["registry-docs", "htpasswd", "owasp-passwords"], "sources": ["registry-docs:sa315b3f8bfa6", "htpasswd:scde9004bb967", "owasp-passwords:s0f70ec4aa271"], "status": "REASONED"}, - "registry-private": {"text": "Use private networking and tunnels/Access with fronting MFA.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, - "filebrowser-admin": {"text": "Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, - "filebrowser-private": {"text": "Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, - "nodered-default": {"text": "Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "nodered-auth": {"text": "Set adminAuth with bcrypt passwords from node-red admin hash-pw.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "nodered-secret": {"text": "Set credentialSecret yourself; otherwise generated, and stored credentials depend on it.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "nodered-private": {"text": "Keep editor private with fronting MFA; it has the power of its flows.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, - "verify-listeners": {"text": "Read every panel/API listener; expect loopback or absence, never wildcard exposure.", "components": ["portainer", "dokploy", "npm", "kuma", "docker"], "sources": ["portainer:s3e163c7d3c38", "dokploy:sc2cc854c9b7e", "npm:se553d384e783", "kuma:s05b853ba300b", "docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, - "verify-panel": {"text": "Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded.", "components": ["portainer", "jenkins", "gitea"], "sources": ["portainer:s3e163c7d3c38", "jenkins:sdfee751aeeee", "gitea:s57933519c308"], "status": "REASONED", "verify": [1]}, - "verify-docker-plain": {"text": "Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive.", "components": ["docker"], "sources": ["docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, - "verify-docker-mtls": {"text": "2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED", "verify": [1]}, - "verify-gitlab": {"text": "Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED", "verify": [1]} - } -} ---- # DevOps panels: Portainer, Coolify, Dokploy, Nginx Proxy Manager, Vaultwarden, Kubernetes Dashboard, Jenkins, Gitea, Uptime Kuma, and the Docker API - -**Version basis** +Written against Portainer (no recorded version), Dashboard arguments v2.7.0, Gitea listener v1.27.3, Gitea MFA minimum 1.24, and Apache htpasswd 2.4; 2 further sources are pinned to a commit and 14 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 62 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-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. +These panels control hosts, containers, clusters, deploy keys, and secrets; a login to one of them is a login to everything it manages. One rule dominates everything tool-specific below: **a DevOps panel is never reachable from the public internet.** Bind it to loopback or a private interface and reach it through SSH port forwarding, a tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything that must be public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) and the admin login gets MFA ([mfa.md](mfa.md)). Several of these tools create their administrator on first visit, so whoever reaches a fresh install first owns it: create the account before the port is reachable by anyone else. -Components (recorded basis): - -- portainer: Portainer (unknown) -- coolify: Coolify (unknown) -- dokploy: Dokploy (unknown) -- npm: Nginx Proxy Manager (unknown) -- vaultwarden: Vaultwarden docs (unknown) -- vaultwarden-pin: Vaultwarden config (3347698712d3e99e652ad4394ef2fc0ce2800fdd) -- dashboard: Dashboard docs (unknown) -- dashboard-old: Dashboard arguments (v2.7.0) -- jenkins: Jenkins (unknown) -- gitea: Gitea docs (unknown) -- gitlab: GitLab CE (unknown) -- kuma: Uptime Kuma (unknown) -- docker: Docker Engine (unknown) -- dozzle: Dozzle (unknown) -- filebrowser: Filebrowser (unknown) -- nodered: Node-RED (unknown) -- gitea-pin: Gitea listener (v1.27.3) -- gitea-mfa-min: Gitea MFA minimum (1.24) -- registry-docs: Docker Registry documentation (rolling) (unknown) -- htpasswd: Apache htpasswd (2.4) -- owasp-passwords: OWASP password storage (1a3f58ed2714f258a1d713966457f1a0f426e841) - -Claims (full source mapping in the front matter): - -- private (REASONED; portainer, docker): Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure. -- portainer-ports (REASONED; portainer): UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute. -- portainer-setup (REASONED; portainer): Setup requires the logged setup_token; first user is admin with a password of at least 12 characters. -- portainer-auth (REASONED; portainer): Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider. -- portainer-socket (REASONED; portainer, docker): Mounted docker.sock grants host-root power; treat a Portainer admin accordingly. -- coolify-setup (REASONED; coolify): HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately. -- coolify-tls (REASONED; coolify): Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close. -- coolify-firewall (REASONED; coolify): Docker bypasses UFW; restrict dashboard ports at the cloud firewall. -- coolify-ssh (REASONED; coolify): Server SSH keys require no passphrase; stored key material is the whole secret. -- dokploy-ports (REASONED; dokploy): UI is 3000, Traefik 80/443; first setup creates admin. -- dokploy-tls (REASONED; dokploy): Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000. -- npm-ports (REASONED; npm): Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in. -- npm-admin (REASONED; npm): Change default first-run admin credentials before other work. -- vaultwarden-tls (REASONED; vaultwarden, vaultwarden-pin): Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com. -- vaultwarden-signup (REASONED; vaultwarden, vaultwarden-pin): SIGNUPS_ALLOWED defaults true; set false to close registration. -- vaultwarden-invites (REASONED; vaultwarden, vaultwarden-pin): INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains. -- vaultwarden-admin (REASONED; vaultwarden, vaultwarden-pin): ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$. -- vaultwarden-session (REASONED; vaultwarden, vaultwarden-pin): Admin sessions expire after 20 minutes by default. -- vaultwarden-port (REASONED; vaultwarden-pin): Container port 80 or non-Docker 8000; publish only to loopback/proxy network. -- dashboard-lifecycle (REASONED; dashboard): Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp. -- dashboard-forward (REASONED; dashboard): Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine. -- dashboard-token (REASONED; dashboard): Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only. -- dashboard-old-flags (REASONED; dashboard-old): Older 2.x enable-skip-login and enable-insecure-login default false; keep both false. -- dashboard-mfa (REASONED; dashboard): API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI. -- jenkins-auth (REASONED; jenkins): Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too. -- jenkins-matrix (REASONED; jenkins): Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control. -- jenkins-csrf (REASONED; jenkins): Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch. -- jenkins-agents (REASONED; jenkins): Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port. -- jenkins-mfa (REASONED; jenkins): No native second factor; enforce MFA through SSO/fronting identity. -- gitea-bind (REASONED; gitea-pin): HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy. -- gitea-tls (REASONED; gitea): Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE. -- gitea-registration (REASONED; gitea): Set service DISABLE_REGISTRATION=true; default false. -- gitea-private (REASONED; gitea): Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false. -- gitea-mfa (REASONED; gitea, gitea-mfa-min): Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later. -- gitea-tokens (REASONED; gitea): With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens. -- gitlab-root (REASONED; gitlab): Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG. -- gitlab-signup (REASONED; gitlab): signup_enabled defaults true; disable for a private forge. -- gitlab-approval (REASONED; gitlab): New instances require admin approval after signup by default; do not rely on approval alone. -- gitlab-visibility (REASONED; gitlab): Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants. -- gitlab-mfa (REASONED; gitlab): Enforce two-factor authentication for every user; available on all tiers. -- kuma-proxy (REASONED; kuma): Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required. -- kuma-admin (REASONED; kuma): Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public. -- docker-root (REASONED; docker): Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375. -- docker-ssh (REASONED; docker): DOCKER_HOST/context can select SSH to the Unix socket without a new listener. -- docker-mtls (REASONED; docker): 2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too. -- dozzle-socket (REASONED; dozzle, docker): Docker-socket access has host-level power; keep Dozzle private with fronting MFA. -- dozzle-auth (REASONED; dozzle): Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy. -- dozzle-actions (REASONED; dozzle): Leave optional container actions/shell off unless needed; either enables remote command execution. -- registry-auth (REASONED; registry-docs): registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution. -- registry-bcrypt (REASONED; registry-docs, htpasswd, owasp-passwords): Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ. -- registry-private (REASONED; registry-docs): Use private networking and tunnels/Access with fronting MFA. -- filebrowser-admin (REASONED; filebrowser): Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure. -- filebrowser-private (REASONED; filebrowser): Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA. -- nodered-default (REASONED; nodered): Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification. -- nodered-auth (REASONED; nodered): Set adminAuth with bcrypt passwords from node-red admin hash-pw. -- nodered-secret (REASONED; nodered): Set credentialSecret yourself; otherwise generated, and stored credentials depend on it. -- nodered-private (REASONED; nodered): Keep editor private with fronting MFA; it has the power of its flows. -- verify-listeners (REASONED; portainer, dokploy, npm, kuma, docker): Read every panel/API listener; expect loopback or absence, never wildcard exposure. -- verify-panel (REASONED; portainer, jenkins, gitea): Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded. -- verify-docker-plain (REASONED; docker): Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive. -- verify-docker-mtls (REASONED; docker): 2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200. -- verify-gitlab (REASONED; gitlab): Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result. - - -These panels control hosts, containers, clusters, deploy keys, and secrets; a login to one of them is a login to everything it manages. One rule dominates everything tool-specific below: **a DevOps panel is never reachable from the public internet.** Bind it to loopback or a private interface and reach it through SSH port forwarding, a tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything that must be public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) and the admin login gets MFA ([mfa.md](mfa.md)). Several of these tools create their administrator on first visit, so whoever reaches a fresh install first owns it: create the account before the port is reachable by anyone else. - -## Portainer +## Portainer - Serves the UI over HTTPS on `9443` (self-signed by default; supply your own certificate or front it per [nginx.md](nginx.md)). `9000` is the legacy HTTP port and stays unpublished; `8000` is the Edge agent tunnel and is only published when Edge Compute is in use. - New instances require a setup token to complete first-time setup; it is in the server logs on the `setup_token=` line. The first user is an administrator, and its password must be at least 12 characters. @@ -509,3 +178,360 @@ REASONED: preceding block; listener and panel-login checks, Docker 2375 refusal - Gitea `HTTP_ADDR` default `0.0.0.0` and `HTTP_PORT` default `3000` (pinned tag v1.27.3): https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122 - Apache 2.4 `htpasswd` bcrypt default cost and `-C` override (checked October 2026): https://httpd.apache.org/docs/2.4/programs/htpasswd.html - OWASP password storage at commit 1a3f58ed2714f258a1d713966457f1a0f426e841 (bcrypt minimum work factor 10) (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139 + + + +## 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/devops-uis.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): + +- portainer: Portainer (unknown) +- dashboard-old: Dashboard arguments (v2.7.0) +- gitea-pin: Gitea listener (v1.27.3) +- gitea-mfa-min: Gitea MFA minimum (1.24) +- coolify: Coolify (unknown) +- dokploy: Dokploy (unknown) +- npm: Nginx Proxy Manager (unknown) +- vaultwarden: Vaultwarden docs (unknown) +- vaultwarden-pin: Vaultwarden config (3347698712d3e99e652ad4394ef2fc0ce2800fdd) +- dashboard: Dashboard docs (unknown) +- jenkins: Jenkins (unknown) +- gitea: Gitea docs (unknown) +- kuma: Uptime Kuma (unknown) +- docker: Docker Engine (unknown) +- gitlab: GitLab CE (unknown) +- dozzle: Dozzle (unknown) +- filebrowser: Filebrowser (unknown) +- nodered: Node-RED (unknown) +- registry-docs: Docker Registry documentation (rolling) (unknown) +- htpasswd: Apache htpasswd (2.4) +- owasp-passwords: OWASP password storage (1a3f58ed2714f258a1d713966457f1a0f426e841) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; portainer, docker): Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure. +- portainer-ports (REASONED; portainer): UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute. +- portainer-setup (REASONED; portainer): Setup requires the logged setup_token; first user is admin with a password of at least 12 characters. +- portainer-auth (REASONED; portainer): Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider. +- portainer-socket (REASONED; portainer, docker): Mounted docker.sock grants host-root power; treat a Portainer admin accordingly. +- coolify-setup (REASONED; coolify): HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately. +- coolify-tls (REASONED; coolify): Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close. +- coolify-firewall (REASONED; coolify): Docker bypasses UFW; restrict dashboard ports at the cloud firewall. +- coolify-ssh (REASONED; coolify): Server SSH keys require no passphrase; stored key material is the whole secret. +- dokploy-ports (REASONED; dokploy): UI is 3000, Traefik 80/443; first setup creates admin. +- dokploy-tls (REASONED; dokploy): Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000. +- npm-ports (REASONED; npm): Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in. +- npm-admin (REASONED; npm): Change default first-run admin credentials before other work. +- vaultwarden-tls (REASONED; vaultwarden, vaultwarden-pin): Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com. +- vaultwarden-signup (REASONED; vaultwarden, vaultwarden-pin): SIGNUPS_ALLOWED defaults true; set false to close registration. +- vaultwarden-invites (REASONED; vaultwarden, vaultwarden-pin): INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains. +- vaultwarden-admin (REASONED; vaultwarden, vaultwarden-pin): ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$. +- vaultwarden-session (REASONED; vaultwarden, vaultwarden-pin): Admin sessions expire after 20 minutes by default. +- vaultwarden-port (REASONED; vaultwarden-pin): Container port 80 or non-Docker 8000; publish only to loopback/proxy network. +- dashboard-lifecycle (REASONED; dashboard): Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp. +- dashboard-forward (REASONED; dashboard): Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine. +- dashboard-token (REASONED; dashboard): Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only. +- dashboard-old-flags (REASONED; dashboard-old): Older 2.x enable-skip-login and enable-insecure-login default false; keep both false. +- dashboard-mfa (REASONED; dashboard): API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI. +- jenkins-auth (REASONED; jenkins): Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too. +- jenkins-matrix (REASONED; jenkins): Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control. +- jenkins-csrf (REASONED; jenkins): Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch. +- jenkins-agents (REASONED; jenkins): Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port. +- jenkins-mfa (REASONED; jenkins): No native second factor; enforce MFA through SSO/fronting identity. +- gitea-bind (REASONED; gitea-pin): HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy. +- gitea-tls (REASONED; gitea): Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE. +- gitea-registration (REASONED; gitea): Set service DISABLE_REGISTRATION=true; default false. +- gitea-private (REASONED; gitea): Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false. +- gitea-mfa (REASONED; gitea, gitea-mfa-min): Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later. +- gitea-tokens (REASONED; gitea): With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens. +- gitlab-root (REASONED; gitlab): Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG. +- gitlab-signup (REASONED; gitlab): signup_enabled defaults true; disable for a private forge. +- gitlab-approval (REASONED; gitlab): New instances require admin approval after signup by default; do not rely on approval alone. +- gitlab-visibility (REASONED; gitlab): Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants. +- gitlab-mfa (REASONED; gitlab): Enforce two-factor authentication for every user; available on all tiers. +- kuma-proxy (REASONED; kuma): Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required. +- kuma-admin (REASONED; kuma): Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public. +- docker-root (REASONED; docker): Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375. +- docker-ssh (REASONED; docker): DOCKER_HOST/context can select SSH to the Unix socket without a new listener. +- docker-mtls (REASONED; docker): 2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too. +- dozzle-socket (REASONED; dozzle, docker): Docker-socket access has host-level power; keep Dozzle private with fronting MFA. +- dozzle-auth (REASONED; dozzle): Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy. +- dozzle-actions (REASONED; dozzle): Leave optional container actions/shell off unless needed; either enables remote command execution. +- registry-auth (REASONED; registry-docs): registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution. +- registry-bcrypt (REASONED; registry-docs, htpasswd, owasp-passwords): Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ. +- registry-private (REASONED; registry-docs): Use private networking and tunnels/Access with fronting MFA. +- filebrowser-admin (REASONED; filebrowser): Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure. +- filebrowser-private (REASONED; filebrowser): Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA. +- nodered-default (REASONED; nodered): Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification. +- nodered-auth (REASONED; nodered): Set adminAuth with bcrypt passwords from node-red admin hash-pw. +- nodered-secret (REASONED; nodered): Set credentialSecret yourself; otherwise generated, and stored credentials depend on it. +- nodered-private (REASONED; nodered): Keep editor private with fronting MFA; it has the power of its flows. +- verify-listeners (REASONED; portainer, dokploy, npm, kuma, docker): Read every panel/API listener; expect loopback or absence, never wildcard exposure. +- verify-panel (REASONED; portainer, jenkins, gitea): Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded. +- verify-docker-plain (REASONED; docker): Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive. +- verify-docker-mtls (REASONED; docker): 2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200. +- verify-gitlab (REASONED; gitlab): Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "7cea8ce7dc2689ed90673f4d926b8259c0c8f0b24281b47ea184dfe91ffa1289", + "components": { + "portainer": { + "name": "Portainer", + "basis": "unknown", + "sources": { + "s3e163c7d3c38": "https://docs.portainer.io/start/install-ce/server/docker/linux", + "s115489d2f86f": "https://docs.portainer.io/start/install-ce/server/setup", + "s59297757a042": "https://docs.portainer.io/admin/settings/authentication", + "s8901bbe7c3d3": "https://docs.portainer.io/admin/settings/authentication/oauth" + } + }, + "dashboard-old": { + "name": "Dashboard arguments", + "basis": "v2.7.0", + "sources": { + "sacea2944651e": "https://github.com/kubernetes-retired/dashboard/blob/v2.7.0/docs/common/dashboard-arguments.md" + } + }, + "gitea-pin": { + "name": "Gitea listener", + "basis": "v1.27.3", + "sources": { + "s31dd622b1c12": "https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122" + } + }, + "gitea-mfa-min": { + "name": "Gitea MFA minimum", + "basis": "1.24", + "sources": { + "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet" + } + }, + "coolify": { + "name": "Coolify", + "basis": "unknown", + "sources": { + "s41e2d21202d9": "https://coolify.io/docs/start-with-self-hosted", + "sf2ae62425b35": "https://coolify.io/docs/core/infrastructure/servers/firewall", + "sf5d819d5d87d": "https://coolify.io/docs/core/networking/proxy/overview", + "s90addf9d8d5d": "https://coolify.io/docs/core/networking/dns", + "s683244a61c48": "https://coolify.io/docs/core/infrastructure/servers/openssh" + } + }, + "dokploy": { + "name": "Dokploy", + "basis": "unknown", + "sources": { + "sc2cc854c9b7e": "https://docs.dokploy.com/docs/core/installation" + } + }, + "npm": { + "name": "Nginx Proxy Manager", + "basis": "unknown", + "sources": { + "se553d384e783": "https://nginxproxymanager.com/setup/" + } + }, + "vaultwarden": { + "name": "Vaultwarden docs", + "basis": "unknown", + "sources": { + "s0bbe8ad856cd": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page", + "s6d3d0d02dcc9": "https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users", + "s60d064cae50e": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS" + } + }, + "vaultwarden-pin": { + "name": "Vaultwarden config", + "basis": "3347698712d3e99e652ad4394ef2fc0ce2800fdd", + "sources": { + "s31c5fc79f5a0": "https://github.com/dani-garcia/vaultwarden/blob/3347698712d3e99e652ad4394ef2fc0ce2800fdd/.env.template" + } + }, + "dashboard": { + "name": "Dashboard docs", + "basis": "unknown", + "sources": { + "s73a36a43f6b5": "https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/" + } + }, + "jenkins": { + "name": "Jenkins", + "basis": "unknown", + "sources": { + "s524c8d5871f1": "https://www.jenkins.io/doc/book/security/managing-security/", + "sdfee751aeeee": "https://www.jenkins.io/doc/book/security/access-control/", + "s6f5287fd36d4": "https://www.jenkins.io/doc/book/security/csrf-protection/" + } + }, + "gitea": { + "name": "Gitea docs", + "basis": "unknown", + "sources": { + "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet", + "s3b7060d58383": "https://docs.gitea.com/usage/user-setting/multi-factor-authentication/" + } + }, + "kuma": { + "name": "Uptime Kuma", + "basis": "unknown", + "sources": { + "s05b853ba300b": "https://github.com/louislam/uptime-kuma", + "sed52bfca4039": "https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" + } + }, + "docker": { + "name": "Docker Engine", + "basis": "unknown", + "sources": { + "sfe15a8156c18": "https://docs.docker.com/engine/security/protect-access/", + "s1b5ec54421b0": "https://docs.docker.com/engine/daemon/remote-access/" + } + }, + "gitlab": { + "name": "GitLab CE", + "basis": "unknown", + "sources": { + "s3b7ac561a92e": "https://docs.gitlab.com/install/docker/installation/", + "s6e8d16cad2ef": "https://docs.gitlab.com/administration/settings/sign_up_restrictions/", + "s6b8a68a77e17": "https://docs.gitlab.com/administration/settings/visibility_and_access_controls/", + "s0347014ce554": "https://docs.gitlab.com/security/two_factor_authentication/" + } + }, + "dozzle": { + "name": "Dozzle", + "basis": "unknown", + "sources": { + "s45b3eab2515b": "https://dozzle.dev/guide/authentication" + } + }, + "filebrowser": { + "name": "Filebrowser", + "basis": "unknown", + "sources": { + "s6386c9e2356b": "https://github.com/filebrowser/filebrowser" + } + }, + "nodered": { + "name": "Node-RED", + "basis": "unknown", + "sources": { + "sa94f329023fe": "https://nodered.org/docs/user-guide/runtime/securing-node-red" + } + }, + "registry-docs": { + "name": "Docker Registry documentation (rolling)", + "basis": "unknown", + "sources": { + "sa315b3f8bfa6": "https://distribution.github.io/distribution/about/deploying/" + } + }, + "htpasswd": { + "name": "Apache htpasswd", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "owasp-passwords": { + "name": "OWASP password storage", + "basis": "1a3f58ed2714f258a1d713966457f1a0f426e841", + "sources": { + "s0f70ec4aa271": "https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139" + } + } + }, + "claims": { + "private": {"text": "Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, + "portainer-ports": {"text": "UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute.", "components": ["portainer"], "sources": ["portainer:s3e163c7d3c38"], "status": "REASONED"}, + "portainer-setup": {"text": "Setup requires the logged setup_token; first user is admin with a password of at least 12 characters.", "components": ["portainer"], "sources": ["portainer:s115489d2f86f"], "status": "REASONED"}, + "portainer-auth": {"text": "Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider.", "components": ["portainer"], "sources": ["portainer:s59297757a042", "portainer:s8901bbe7c3d3"], "status": "REASONED"}, + "portainer-socket": {"text": "Mounted docker.sock grants host-root power; treat a Portainer admin accordingly.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, + "coolify-setup": {"text": "HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately.", "components": ["coolify"], "sources": ["coolify:s41e2d21202d9"], "status": "REASONED"}, + "coolify-tls": {"text": "Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35", "coolify:sf5d819d5d87d", "coolify:s90addf9d8d5d"], "status": "REASONED"}, + "coolify-firewall": {"text": "Docker bypasses UFW; restrict dashboard ports at the cloud firewall.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35"], "status": "REASONED"}, + "coolify-ssh": {"text": "Server SSH keys require no passphrase; stored key material is the whole secret.", "components": ["coolify"], "sources": ["coolify:s683244a61c48"], "status": "REASONED"}, + "dokploy-ports": {"text": "UI is 3000, Traefik 80/443; first setup creates admin.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, + "dokploy-tls": {"text": "Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, + "npm-ports": {"text": "Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, + "npm-admin": {"text": "Change default first-run admin credentials before other work.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, + "vaultwarden-tls": {"text": "Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s60d064cae50e", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-signup": {"text": "SIGNUPS_ALLOWED defaults true; set false to close registration.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-invites": {"text": "INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-admin": {"text": "ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-session": {"text": "Admin sessions expire after 20 minutes by default.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-port": {"text": "Container port 80 or non-Docker 8000; publish only to loopback/proxy network.", "components": ["vaultwarden-pin"], "sources": ["vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "dashboard-lifecycle": {"text": "Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-forward": {"text": "Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-token": {"text": "Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-old-flags": {"text": "Older 2.x enable-skip-login and enable-insecure-login default false; keep both false.", "components": ["dashboard-old"], "sources": ["dashboard-old:sacea2944651e"], "status": "REASONED"}, + "dashboard-mfa": {"text": "API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "jenkins-auth": {"text": "Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, + "jenkins-matrix": {"text": "Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control.", "components": ["jenkins"], "sources": ["jenkins:sdfee751aeeee"], "status": "REASONED"}, + "jenkins-csrf": {"text": "Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch.", "components": ["jenkins"], "sources": ["jenkins:s6f5287fd36d4"], "status": "REASONED"}, + "jenkins-agents": {"text": "Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1"], "status": "REASONED"}, + "jenkins-mfa": {"text": "No native second factor; enforce MFA through SSO/fronting identity.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, + "gitea-bind": {"text": "HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy.", "components": ["gitea-pin"], "sources": ["gitea-pin:s31dd622b1c12"], "status": "REASONED"}, + "gitea-tls": {"text": "Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-registration": {"text": "Set service DISABLE_REGISTRATION=true; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-private": {"text": "Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-mfa": {"text": "Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later.", "components": ["gitea", "gitea-mfa-min"], "sources": ["gitea:s57933519c308", "gitea:s3b7060d58383", "gitea-mfa-min:s57933519c308"], "status": "REASONED"}, + "gitea-tokens": {"text": "With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens.", "components": ["gitea"], "sources": ["gitea:s3b7060d58383"], "status": "REASONED"}, + "gitlab-root": {"text": "Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED"}, + "gitlab-signup": {"text": "signup_enabled defaults true; disable for a private forge.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, + "gitlab-approval": {"text": "New instances require admin approval after signup by default; do not rely on approval alone.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, + "gitlab-visibility": {"text": "Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants.", "components": ["gitlab"], "sources": ["gitlab:s6b8a68a77e17"], "status": "REASONED"}, + "gitlab-mfa": {"text": "Enforce two-factor authentication for every user; available on all tiers.", "components": ["gitlab"], "sources": ["gitlab:s0347014ce554"], "status": "REASONED"}, + "kuma-proxy": {"text": "Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b", "kuma:sed52bfca4039"], "status": "REASONED"}, + "kuma-admin": {"text": "Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b"], "status": "REASONED"}, + "docker-root": {"text": "Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375.", "components": ["docker"], "sources": ["docker:sfe15a8156c18", "docker:s1b5ec54421b0"], "status": "REASONED"}, + "docker-ssh": {"text": "DOCKER_HOST/context can select SSH to the Unix socket without a new listener.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, + "docker-mtls": {"text": "2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, + "dozzle-socket": {"text": "Docker-socket access has host-level power; keep Dozzle private with fronting MFA.", "components": ["dozzle", "docker"], "sources": ["dozzle:s45b3eab2515b", "docker:sfe15a8156c18"], "status": "REASONED"}, + "dozzle-auth": {"text": "Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, + "dozzle-actions": {"text": "Leave optional container actions/shell off unless needed; either enables remote command execution.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, + "registry-auth": {"text": "registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, + "registry-bcrypt": {"text": "Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ.", "components": ["registry-docs", "htpasswd", "owasp-passwords"], "sources": ["registry-docs:sa315b3f8bfa6", "htpasswd:scde9004bb967", "owasp-passwords:s0f70ec4aa271"], "status": "REASONED"}, + "registry-private": {"text": "Use private networking and tunnels/Access with fronting MFA.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, + "filebrowser-admin": {"text": "Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, + "filebrowser-private": {"text": "Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, + "nodered-default": {"text": "Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-auth": {"text": "Set adminAuth with bcrypt passwords from node-red admin hash-pw.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-secret": {"text": "Set credentialSecret yourself; otherwise generated, and stored credentials depend on it.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-private": {"text": "Keep editor private with fronting MFA; it has the power of its flows.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "verify-listeners": {"text": "Read every panel/API listener; expect loopback or absence, never wildcard exposure.", "components": ["portainer", "dokploy", "npm", "kuma", "docker"], "sources": ["portainer:s3e163c7d3c38", "dokploy:sc2cc854c9b7e", "npm:se553d384e783", "kuma:s05b853ba300b", "docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, + "verify-panel": {"text": "Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded.", "components": ["portainer", "jenkins", "gitea"], "sources": ["portainer:s3e163c7d3c38", "jenkins:sdfee751aeeee", "gitea:s57933519c308"], "status": "REASONED", "verify": [1]}, + "verify-docker-plain": {"text": "Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive.", "components": ["docker"], "sources": ["docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, + "verify-docker-mtls": {"text": "2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED", "verify": [1]}, + "verify-gitlab": {"text": "Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/dotnet.md b/plugin/skills/secure-deployment-config/references/dotnet.md index a7ac80a0d..0928ae317 100644 --- a/plugin/skills/secure-deployment-config/references/dotnet.md +++ b/plugin/skills/secure-deployment-config/references/dotnet.md @@ -1,318 +1,22 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "10f455bd5e1e9cc54ea69a2d50883c9cba79de0ada5085b56891741f8232ee5d", - "components": { - "docs": { - "name": "ASP.NET Core documentation", - "basis": ".NET 10", - "sources": { - "s8c762260991c": "https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-10.0", - "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", - "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", - "s3790c49f70b9": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-10.0", - "s9d10c561bb48": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions?view=aspnetcore-10.0", - "sa3c22a38f4cb": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-10.0", - "sb6780977fa3f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy?view=aspnetcore-10.0", - "s592dc85bb71f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0", - "s6afc0255098f": "https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.dangerousacceptanyservercertificatevalidator?view=net-10.0", - "s2d5a930aac35": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-10.0" - } - }, - "rate-min": { - "name": "ASP.NET Core rate-limiter minimum", - "basis": ".NET 7", - "sources": { - "sd3fef8f45066": "https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit" - } - }, - "net10": { - "name": "ASP.NET Core", - "basis": ".NET 10", - "sources": { - "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", - "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", - "s0d7c367ac06f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-10.0", - "sc0654e58fc4c": "https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-10.0", - "s837df4b1657f": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints?view=aspnetcore-10.0", - "s2f7e6a8213f1": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-10.0", - "s61afb89034f8": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0", - "s9d290374f4e8": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint?view=aspnetcore-10.0", - "s1ce47370d948": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-10.0", - "s55f298bf0048": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.disablerequestsizelimitattribute?view=aspnetcore-10.0", - "s792d3e8f3e42": "https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-10.0", - "s529a3d7cce08": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0", - "s6682926c8da7": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-10.0", - "s66f1ab4555d6": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-10.0", - "sd635cefc0a9f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate?view=aspnetcore-10.0", - "s9463626a3d0e": "https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-10.0", - "s1d58219cedec": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0", - "s1f9d4c5e975c": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0", - "sc41b77cc06c8": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0", - "se101626c29d6": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0" - } - }, - "source": { - "name": "ASP.NET Core source", - "basis": "v10.0.0", - "sources": { - "sb62cff70e3b8": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpsPolicy/src/HstsOptions.cs", - "sfc86e3f62243": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs", - "s903da00d32b5": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs", - "sc00d8d67e93b": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/Http2Limits.cs", - "s67f347ba7947": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Http/Http/src/Features/FormOptions.cs", - "s18c44d6c2fb7": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs" - } - }, - "proxy-fix": { - "name": "ASP.NET Core proxy hardening", - "basis": "8.0.17", - "sources": { - "s2864662bcd21": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/8/forwarded-headers-unknown-proxies?view=aspnetcore-10.0" - } - }, - "runtime": { - "name": "NET runtime source", - "basis": "v10.0.0", - "sources": { - "s29ee940c94b9": "https://raw.githubusercontent.com/dotnet/runtime/v10.0.0/src/native/libs/System.Security.Cryptography.Native/pal_x509_root.c" - } - }, - "openssl": { - "name": "OpenSSL documentation", - "basis": "3.0", - "sources": { - "scac6dada5f6a": "https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/" - } - }, - "precedence": { - "name": "WebApplicationBuilder precedence", - "basis": ".NET 7", - "sources": { - "s10d46bc97fef": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/7/environment-variable-precedence?view=aspnetcore-10.0" - } - }, - "iis": { - "name": "IIS documentation", - "basis": "IIS 10", - "sources": { - "sb2eac755b583": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/" - } - }, - "iis-rolling": { - "name": "IIS documentation (rolling)", - "basis": "unknown", - "sources": { - "s267eddd43502": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/httpprotocol/customheaders/" - } - }, - "dotnet-rolling": { - "name": ".NET documentation (rolling)", - "basis": "unknown", - "sources": { - "s640872a17a8a": "https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography" - } - }, - "cf-docs": { - "name": "Cloudflare 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", - "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", - "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", - "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" - } - } - }, - "claims": { - "tls": {"text": "Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED"}, - "tls-protocols": {"text": "Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3.", "components": ["net10"], "sources": ["net10:s74dcd6a72d63"], "status": "REASONED"}, - "hsts-age": {"text": "NET 10 HSTS defaults to 30 days; example selects 365 days outside Development.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, - "hsts-subdomains": {"text": "HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, - "hsts-preload": {"text": "HSTS Preload defaults false.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, - "redirect": {"text": "UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED"}, - "proxy-bind": {"text": "ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s5701d26de241"], "status": "REASONED"}, - "proxy-default": {"text": "NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, - "proxy-networks": {"text": "NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, - "proxy-hardening": {"text": "Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy.", "components": ["net10", "proxy-fix"], "sources": ["net10:s5701d26de241", "proxy-fix:s2864662bcd21"], "status": "REASONED"}, - "proxy-https": {"text": "When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops.", "components": ["docs"], "sources": ["docs:s8c762260991c", "docs:s5701d26de241"], "status": "REASONED"}, - "password": {"text": "Identity uses PBKDF2 with default 100000 iterations; keep its password hasher.", "components": ["docs"], "sources": ["docs:s3790c49f70b9", "docs:s9d10c561bb48"], "status": "REASONED"}, - "lockout": {"text": "Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false.", "components": ["net10"], "sources": ["net10:s0d7c367ac06f"], "status": "REASONED"}, - "cookies": {"text": "Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options.", "components": ["docs", "net10"], "sources": ["docs:sa3c22a38f4cb", "docs:sb6780977fa3f", "net10:s0d7c367ac06f"], "status": "REASONED"}, - "samesite": {"text": "NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, - "remote-cookies": {"text": "Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, - "authorization": {"text": "Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages.", "components": ["docs"], "sources": ["docs:sa3c22a38f4cb"], "status": "REASONED"}, - "login-limit": {"text": "NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing.", "components": ["rate-min"], "sources": ["rate-min:sd3fef8f45066"], "status": "REASONED"}, - "oidc": {"text": "Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json.", "components": ["docs"], "sources": ["docs:s592dc85bb71f"], "status": "REASONED"}, - "mfa": {"text": "Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation.", "components": ["docs", "cf-docs"], "sources": ["docs:s592dc85bb71f", "docs:s2d5a930aac35", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "cf-docs:sfddb92e438ca", "cf-docs:s863803bd9a1f"], "status": "REASONED"}, - "client-validation": {"text": "Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback.", "components": ["docs"], "sources": ["docs:s6afc0255098f"], "status": "REASONED"}, - "client-ca": {"text": "On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots.", "components": ["runtime", "openssl", "dotnet-rolling"], "sources": ["dotnet-rolling:s640872a17a8a", "runtime:s29ee940c94b9", "openssl:scac6dada5f6a"], "status": "REASONED"}, - "developer-errors": {"text": "Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s61afb89034f8"], "status": "REASONED"}, - "error-handler": {"text": "Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED"}, - "environment": {"text": "Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs.", "components": ["net10", "precedence"], "sources": ["net10:s61afb89034f8", "precedence:s10d46bc97fef"], "status": "REASONED"}, - "database-diagnostics": {"text": "Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s9d290374f4e8"], "status": "REASONED"}, - "body-limit": {"text": "NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "body-override": {"text": "RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s55f298bf0048"], "status": "REASONED"}, - "multipart": {"text": "MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413.", "components": ["source", "net10"], "sources": ["source:s67f347ba7947", "net10:s792d3e8f3e42"], "status": "REASONED"}, - "iis-body": {"text": "In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s792d3e8f3e42", "net10:s529a3d7cce08"], "status": "REASONED"}, - "keepalive": {"text": "Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "header-timeout": {"text": "Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "connections": {"text": "MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "upgraded": {"text": "Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, - "header-count": {"text": "Kestrel MaxRequestHeaderCount defaults 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "header-size": {"text": "Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "request-rate": {"text": "MinRequestBodyDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "response-rate": {"text": "MinResponseDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, - "rate-scope": {"text": "HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED"}, - "h2-streams": {"text": "NET 10 HTTP/2 MaxStreamsPerConnection defaults 100.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-table": {"text": "NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-frame": {"text": "NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-header": {"text": "NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-connection-window": {"text": "NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-stream-window": {"text": "NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-ping-delay": {"text": "NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-ping-timeout": {"text": "NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, - "h2-scope": {"text": "Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:sc00d8d67e93b"], "status": "REASONED"}, - "key-defaults": {"text": "Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys.", "components": ["net10"], "sources": ["net10:s66f1ab4555d6"], "status": "REASONED"}, - "key-persistence": {"text": "Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, - "key-encryption": {"text": "Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, - "key-isolation": {"text": "Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys.", "components": ["net10"], "sources": ["net10:s6682926c8da7"], "status": "REASONED"}, - "cookie-sharing": {"text": "Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration.", "components": ["net10"], "sources": ["net10:s9463626a3d0e"], "status": "REASONED"}, - "server-header": {"text": "NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access.", "components": ["source"], "sources": ["source:s18c44d6c2fb7"], "status": "REASONED"}, - "iis-headers": {"text": "Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later.", "components": ["iis", "iis-rolling"], "sources": ["iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED"}, - "openapi": {"text": "NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy.", "components": ["net10"], "sources": ["net10:s1d58219cedec"], "status": "REASONED"}, - "swagger": {"text": "Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document.", "components": ["net10"], "sources": ["net10:s1f9d4c5e975c"], "status": "REASONED"}, - "health": {"text": "Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, - "health-host": {"text": "RequireHost checks a spoofable Host header and is not a network or authentication boundary.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, - "host-filter": {"text": "AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate.", "components": ["net10"], "sources": ["net10:s5701d26de241", "net10:se101626c29d6"], "status": "REASONED"}, - "verify-redirect": {"text": "HTTP should return 307/308 with HTTPS Location.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED", "verify": [1]}, - "verify-tls": {"text": "HTTPS must validate without -k and show HSTS.", "components": ["docs", "net10"], "sources": ["docs:s8c762260991c", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, - "verify-api": {"text": "Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first.", "components": ["net10"], "sources": ["net10:s837df4b1657f"], "status": "REASONED", "verify": [1]}, - "verify-bind": {"text": "Inspect every listener for 127.0.0.1 and ::1 behind the proxy.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, - "verify-errors": {"text": "Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED", "verify": [2]}, - "verify-body": {"text": "Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED", "verify": [3]}, - "verify-aux": {"text": "Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization.", "components": ["net10"], "sources": ["net10:s1d58219cedec", "net10:s1f9d4c5e975c", "net10:sc41b77cc06c8"], "status": "REASONED", "verify": [4]}, - "verify-migrations": {"text": "GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure.", "components": ["net10"], "sources": ["net10:s9d290374f4e8"], "status": "REASONED"}, - "verify-headers": {"text": "Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported.", "components": ["source", "iis", "iis-rolling"], "sources": ["source:s18c44d6c2fb7", "iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED", "verify": [5]}, - "verify-cookie": {"text": "Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes.", "components": ["net10"], "sources": ["net10:s837df4b1657f", "net10:s66f1ab4555d6", "net10:s9463626a3d0e"], "status": "REASONED", "verify": [6]}, - "verify-antiforgery": {"text": "Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:s66f1ab4555d6"], "status": "REASONED"} - } -} ---- # ASP.NET Core and Kestrel: TLS and authentication - -**Version basis** +Written against ASP.NET Core documentation .NET 10, ASP.NET Core rate-limiter minimum .NET 7, ASP.NET Core .NET 10, ASP.NET Core source v10.0.0, ASP.NET Core proxy hardening 8.0.17, NET runtime source v10.0.0, WebApplicationBuilder precedence .NET 7, and OpenSSL documentation 3.0, plus 1 more recorded basis; 1 further source is pinned to a commit and 2 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 71 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-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. +Preferred production layout: bind Kestrel to loopback and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). Kestrel can also terminate TLS itself, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). -Components (recorded basis): +## 1. HTTPS directly in Kestrel -- docs: ASP.NET Core documentation (.NET 10) -- rate-min: ASP.NET Core rate-limiter minimum (.NET 7) -- net10: ASP.NET Core (.NET 10) -- source: ASP.NET Core source (v10.0.0) -- proxy-fix: ASP.NET Core proxy hardening (8.0.17) -- runtime: NET runtime source (v10.0.0) -- openssl: OpenSSL documentation (3.0) -- precedence: WebApplicationBuilder precedence (.NET 7) -- iis: IIS documentation (IIS 10) -- iis-rolling: IIS documentation (rolling) (unknown) -- dotnet-rolling: .NET documentation (rolling) (unknown) -- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +`appsettings.json` with a PKCS12 file (for PEM files, `Path` is the certificate and `KeyPath` the private key; the docs warn against a plaintext `Password` here, so supply it from the environment or a secret store): -Claims (full source mapping in the front matter): - -- tls (REASONED; docs, net10): Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store. -- tls-protocols (REASONED; net10): Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3. -- hsts-age (REASONED; source): NET 10 HSTS defaults to 30 days; example selects 365 days outside Development. -- hsts-subdomains (REASONED; source): HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS. -- hsts-preload (REASONED; source): HSTS Preload defaults false. -- redirect (REASONED; docs): UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort. -- proxy-bind (REASONED; docs, net10): ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first. -- proxy-default (REASONED; net10, source): NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list. -- proxy-networks (REASONED; net10, source): NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty. -- proxy-hardening (REASONED; net10, proxy-fix): Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy. -- proxy-https (REASONED; docs): When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops. -- password (REASONED; docs): Identity uses PBKDF2 with default 100000 iterations; keep its password hasher. -- lockout (REASONED; net10): Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false. -- cookies (REASONED; docs, net10): Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options. -- samesite (REASONED; net10): NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks. -- remote-cookies (REASONED; net10): Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting. -- authorization (REASONED; docs): Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages. -- login-limit (REASONED; rate-min): NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing. -- oidc (REASONED; docs): Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json. -- mfa (REASONED; docs, cf-docs): Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation. -- client-validation (REASONED; docs): Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback. -- client-ca (REASONED; runtime, openssl, dotnet-rolling): On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots. -- developer-errors (REASONED; net10): Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment. -- error-handler (REASONED; net10): Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses. -- environment (REASONED; net10, precedence): Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs. -- database-diagnostics (REASONED; net10): Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes. -- body-limit (REASONED; net10, source): NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes. -- body-override (REASONED; net10): RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection. -- multipart (REASONED; source, net10): MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413. -- iis-body (REASONED; net10): In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting. -- keepalive (REASONED; net10, source): Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline. -- header-timeout (REASONED; net10, source): Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline. -- connections (REASONED; source): MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100. -- upgraded (REASONED; net10, source): Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null. -- header-count (REASONED; source): Kestrel MaxRequestHeaderCount defaults 100. -- header-size (REASONED; source): Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes. -- request-rate (REASONED; source): MinRequestBodyDataRate defaults 240 bytes/second with five-second grace. -- response-rate (REASONED; source): MinResponseDataRate defaults 240 bytes/second with five-second grace. -- rate-scope (REASONED; net10): HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger. -- h2-streams (REASONED; source): NET 10 HTTP/2 MaxStreamsPerConnection defaults 100. -- h2-table (REASONED; source): NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes. -- h2-frame (REASONED; source): NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes. -- h2-header (REASONED; source): NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192. -- h2-connection-window (REASONED; source): NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072. -- h2-stream-window (REASONED; source): NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304. -- h2-ping-delay (REASONED; source): NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings. -- h2-ping-timeout (REASONED; source): NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds. -- h2-scope (REASONED; net10, source): Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3. -- key-defaults (REASONED; net10): Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys. -- key-persistence (REASONED; net10): Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names. -- key-encryption (REASONED; net10): Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates. -- key-isolation (REASONED; net10): Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys. -- cookie-sharing (REASONED; net10): Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration. -- server-header (REASONED; source): NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access. -- iis-headers (REASONED; iis, iis-rolling): Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later. -- openapi (REASONED; net10): NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy. -- swagger (REASONED; net10): Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document. -- health (REASONED; net10): Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route. -- health-host (REASONED; net10): RequireHost checks a spoofable Host header and is not a network or authentication boundary. -- host-filter (REASONED; net10): AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate. -- verify-redirect (REASONED; docs): HTTP should return 307/308 with HTTPS Location. -- verify-tls (REASONED; docs, net10): HTTPS must validate without -k and show HSTS. -- verify-api (REASONED; net10): Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first. -- verify-bind (REASONED; docs, net10): Inspect every listener for 127.0.0.1 and ::1 behind the proxy. -- verify-errors (REASONED; net10): Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution. -- verify-body (REASONED; net10): Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection. -- verify-aux (REASONED; net10): Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization. -- verify-migrations (REASONED; net10): GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure. -- verify-headers (REASONED; source, iis, iis-rolling): Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported. -- verify-cookie (REASONED; net10): Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes. -- verify-antiforgery (REASONED; net10): Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery. - - -Preferred production layout: bind Kestrel to loopback and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). Kestrel can also terminate TLS itself, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). - -## 1. HTTPS directly in Kestrel - -`appsettings.json` with a PKCS12 file (for PEM files, `Path` is the certificate and `KeyPath` the private key; the docs warn against a plaintext `Password` here, so supply it from the environment or a secret store): - -```json -{ "Kestrel": { "Endpoints": { - "Http": { "Url": "http://*:80" }, - "Https": { "Url": "https://*:443", - "Certificate": { "Path": "/etc/ssl/private/server.pfx", "Password": "REPLACE_WITH_LONG_RANDOM_VALUE" } } -} } } -``` +```json +{ "Kestrel": { "Endpoints": { + "Http": { "Url": "http://*:80" }, + "Https": { "Url": "https://*:443", + "Certificate": { "Path": "/etc/ssl/private/server.pfx", "Password": "REPLACE_WITH_LONG_RANDOM_VALUE" } } +} } } +``` ```csharp builder.WebHost.ConfigureKestrel(serverOptions => @@ -752,3 +456,325 @@ Also retain an antiforgery token and its accompanying cookie across a restart, t - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, independent MFA and supported TOTP/WebAuthn factors (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24 - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, self-hosted public application authentication layer (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#L14-L16 - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, publicly routable origin protection requirement (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#L48-L48 + + + +## 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/dotnet.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): + +- docs: ASP.NET Core documentation (.NET 10) +- rate-min: ASP.NET Core rate-limiter minimum (.NET 7) +- net10: ASP.NET Core (.NET 10) +- source: ASP.NET Core source (v10.0.0) +- proxy-fix: ASP.NET Core proxy hardening (8.0.17) +- runtime: NET runtime source (v10.0.0) +- precedence: WebApplicationBuilder precedence (.NET 7) +- dotnet-rolling: .NET documentation (rolling) (unknown) +- openssl: OpenSSL documentation (3.0) +- iis: IIS documentation (IIS 10) +- iis-rolling: IIS documentation (rolling) (unknown) +- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) + +Claims (full source mapping in the JSON block below): + +- tls (REASONED; docs, net10): Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store. +- tls-protocols (REASONED; net10): Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3. +- hsts-age (REASONED; source): NET 10 HSTS defaults to 30 days; example selects 365 days outside Development. +- hsts-subdomains (REASONED; source): HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS. +- hsts-preload (REASONED; source): HSTS Preload defaults false. +- redirect (REASONED; docs): UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort. +- proxy-bind (REASONED; docs, net10): ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first. +- proxy-default (REASONED; net10, source): NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list. +- proxy-networks (REASONED; net10, source): NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty. +- proxy-hardening (REASONED; net10, proxy-fix): Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy. +- proxy-https (REASONED; docs): When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops. +- password (REASONED; docs): Identity uses PBKDF2 with default 100000 iterations; keep its password hasher. +- lockout (REASONED; net10): Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false. +- cookies (REASONED; docs, net10): Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options. +- samesite (REASONED; net10): NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks. +- remote-cookies (REASONED; net10): Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting. +- authorization (REASONED; docs): Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages. +- login-limit (REASONED; rate-min): NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing. +- oidc (REASONED; docs): Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json. +- mfa (REASONED; docs, cf-docs): Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation. +- client-validation (REASONED; docs): Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback. +- client-ca (REASONED; runtime, openssl, dotnet-rolling): On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots. +- developer-errors (REASONED; net10): Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment. +- error-handler (REASONED; net10): Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses. +- environment (REASONED; net10, precedence): Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs. +- database-diagnostics (REASONED; net10): Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes. +- body-limit (REASONED; net10, source): NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes. +- body-override (REASONED; net10): RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection. +- multipart (REASONED; source, net10): MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413. +- iis-body (REASONED; net10): In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting. +- keepalive (REASONED; net10, source): Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline. +- header-timeout (REASONED; net10, source): Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline. +- connections (REASONED; source): MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100. +- upgraded (REASONED; net10, source): Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null. +- header-count (REASONED; source): Kestrel MaxRequestHeaderCount defaults 100. +- header-size (REASONED; source): Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes. +- request-rate (REASONED; source): MinRequestBodyDataRate defaults 240 bytes/second with five-second grace. +- response-rate (REASONED; source): MinResponseDataRate defaults 240 bytes/second with five-second grace. +- rate-scope (REASONED; net10): HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger. +- h2-streams (REASONED; source): NET 10 HTTP/2 MaxStreamsPerConnection defaults 100. +- h2-table (REASONED; source): NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes. +- h2-frame (REASONED; source): NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes. +- h2-header (REASONED; source): NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192. +- h2-connection-window (REASONED; source): NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072. +- h2-stream-window (REASONED; source): NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304. +- h2-ping-delay (REASONED; source): NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings. +- h2-ping-timeout (REASONED; source): NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds. +- h2-scope (REASONED; net10, source): Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3. +- key-defaults (REASONED; net10): Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys. +- key-persistence (REASONED; net10): Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names. +- key-encryption (REASONED; net10): Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates. +- key-isolation (REASONED; net10): Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys. +- cookie-sharing (REASONED; net10): Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration. +- server-header (REASONED; source): NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access. +- iis-headers (REASONED; iis, iis-rolling): Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later. +- openapi (REASONED; net10): NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy. +- swagger (REASONED; net10): Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document. +- health (REASONED; net10): Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route. +- health-host (REASONED; net10): RequireHost checks a spoofable Host header and is not a network or authentication boundary. +- host-filter (REASONED; net10): AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate. +- verify-redirect (REASONED; docs): HTTP should return 307/308 with HTTPS Location. +- verify-tls (REASONED; docs, net10): HTTPS must validate without -k and show HSTS. +- verify-api (REASONED; net10): Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first. +- verify-bind (REASONED; docs, net10): Inspect every listener for 127.0.0.1 and ::1 behind the proxy. +- verify-errors (REASONED; net10): Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution. +- verify-body (REASONED; net10): Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection. +- verify-aux (REASONED; net10): Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization. +- verify-migrations (REASONED; net10): GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure. +- verify-headers (REASONED; source, iis, iis-rolling): Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported. +- verify-cookie (REASONED; net10): Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes. +- verify-antiforgery (REASONED; net10): Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "10f455bd5e1e9cc54ea69a2d50883c9cba79de0ada5085b56891741f8232ee5d", + "components": { + "docs": { + "name": "ASP.NET Core documentation", + "basis": ".NET 10", + "sources": { + "s8c762260991c": "https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-10.0", + "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", + "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", + "s3790c49f70b9": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-10.0", + "s9d10c561bb48": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions?view=aspnetcore-10.0", + "sa3c22a38f4cb": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-10.0", + "sb6780977fa3f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy?view=aspnetcore-10.0", + "s592dc85bb71f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0", + "s6afc0255098f": "https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.dangerousacceptanyservercertificatevalidator?view=net-10.0", + "s2d5a930aac35": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-10.0" + } + }, + "rate-min": { + "name": "ASP.NET Core rate-limiter minimum", + "basis": ".NET 7", + "sources": { + "sd3fef8f45066": "https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit" + } + }, + "net10": { + "name": "ASP.NET Core", + "basis": ".NET 10", + "sources": { + "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", + "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", + "s0d7c367ac06f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-10.0", + "sc0654e58fc4c": "https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-10.0", + "s837df4b1657f": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints?view=aspnetcore-10.0", + "s2f7e6a8213f1": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-10.0", + "s61afb89034f8": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0", + "s9d290374f4e8": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint?view=aspnetcore-10.0", + "s1ce47370d948": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-10.0", + "s55f298bf0048": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.disablerequestsizelimitattribute?view=aspnetcore-10.0", + "s792d3e8f3e42": "https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-10.0", + "s529a3d7cce08": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0", + "s6682926c8da7": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-10.0", + "s66f1ab4555d6": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-10.0", + "sd635cefc0a9f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate?view=aspnetcore-10.0", + "s9463626a3d0e": "https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-10.0", + "s1d58219cedec": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0", + "s1f9d4c5e975c": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0", + "sc41b77cc06c8": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0", + "se101626c29d6": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0" + } + }, + "source": { + "name": "ASP.NET Core source", + "basis": "v10.0.0", + "sources": { + "sb62cff70e3b8": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpsPolicy/src/HstsOptions.cs", + "sfc86e3f62243": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs", + "s903da00d32b5": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs", + "sc00d8d67e93b": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/Http2Limits.cs", + "s67f347ba7947": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Http/Http/src/Features/FormOptions.cs", + "s18c44d6c2fb7": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs" + } + }, + "proxy-fix": { + "name": "ASP.NET Core proxy hardening", + "basis": "8.0.17", + "sources": { + "s2864662bcd21": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/8/forwarded-headers-unknown-proxies?view=aspnetcore-10.0" + } + }, + "runtime": { + "name": "NET runtime source", + "basis": "v10.0.0", + "sources": { + "s29ee940c94b9": "https://raw.githubusercontent.com/dotnet/runtime/v10.0.0/src/native/libs/System.Security.Cryptography.Native/pal_x509_root.c" + } + }, + "precedence": { + "name": "WebApplicationBuilder precedence", + "basis": ".NET 7", + "sources": { + "s10d46bc97fef": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/7/environment-variable-precedence?view=aspnetcore-10.0" + } + }, + "dotnet-rolling": { + "name": ".NET documentation (rolling)", + "basis": "unknown", + "sources": { + "s640872a17a8a": "https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "3.0", + "sources": { + "scac6dada5f6a": "https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/" + } + }, + "iis": { + "name": "IIS documentation", + "basis": "IIS 10", + "sources": { + "sb2eac755b583": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/" + } + }, + "iis-rolling": { + "name": "IIS documentation (rolling)", + "basis": "unknown", + "sources": { + "s267eddd43502": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/httpprotocol/customheaders/" + } + }, + "cf-docs": { + "name": "Cloudflare 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", + "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", + "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", + "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" + } + } + }, + "claims": { + "tls": {"text": "Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED"}, + "tls-protocols": {"text": "Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3.", "components": ["net10"], "sources": ["net10:s74dcd6a72d63"], "status": "REASONED"}, + "hsts-age": {"text": "NET 10 HSTS defaults to 30 days; example selects 365 days outside Development.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "hsts-subdomains": {"text": "HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "hsts-preload": {"text": "HSTS Preload defaults false.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "redirect": {"text": "UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED"}, + "proxy-bind": {"text": "ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s5701d26de241"], "status": "REASONED"}, + "proxy-default": {"text": "NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, + "proxy-networks": {"text": "NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, + "proxy-hardening": {"text": "Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy.", "components": ["net10", "proxy-fix"], "sources": ["net10:s5701d26de241", "proxy-fix:s2864662bcd21"], "status": "REASONED"}, + "proxy-https": {"text": "When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops.", "components": ["docs"], "sources": ["docs:s8c762260991c", "docs:s5701d26de241"], "status": "REASONED"}, + "password": {"text": "Identity uses PBKDF2 with default 100000 iterations; keep its password hasher.", "components": ["docs"], "sources": ["docs:s3790c49f70b9", "docs:s9d10c561bb48"], "status": "REASONED"}, + "lockout": {"text": "Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false.", "components": ["net10"], "sources": ["net10:s0d7c367ac06f"], "status": "REASONED"}, + "cookies": {"text": "Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options.", "components": ["docs", "net10"], "sources": ["docs:sa3c22a38f4cb", "docs:sb6780977fa3f", "net10:s0d7c367ac06f"], "status": "REASONED"}, + "samesite": {"text": "NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, + "remote-cookies": {"text": "Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, + "authorization": {"text": "Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages.", "components": ["docs"], "sources": ["docs:sa3c22a38f4cb"], "status": "REASONED"}, + "login-limit": {"text": "NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing.", "components": ["rate-min"], "sources": ["rate-min:sd3fef8f45066"], "status": "REASONED"}, + "oidc": {"text": "Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json.", "components": ["docs"], "sources": ["docs:s592dc85bb71f"], "status": "REASONED"}, + "mfa": {"text": "Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation.", "components": ["docs", "cf-docs"], "sources": ["docs:s592dc85bb71f", "docs:s2d5a930aac35", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "cf-docs:sfddb92e438ca", "cf-docs:s863803bd9a1f"], "status": "REASONED"}, + "client-validation": {"text": "Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback.", "components": ["docs"], "sources": ["docs:s6afc0255098f"], "status": "REASONED"}, + "client-ca": {"text": "On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots.", "components": ["runtime", "openssl", "dotnet-rolling"], "sources": ["dotnet-rolling:s640872a17a8a", "runtime:s29ee940c94b9", "openssl:scac6dada5f6a"], "status": "REASONED"}, + "developer-errors": {"text": "Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s61afb89034f8"], "status": "REASONED"}, + "error-handler": {"text": "Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED"}, + "environment": {"text": "Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs.", "components": ["net10", "precedence"], "sources": ["net10:s61afb89034f8", "precedence:s10d46bc97fef"], "status": "REASONED"}, + "database-diagnostics": {"text": "Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s9d290374f4e8"], "status": "REASONED"}, + "body-limit": {"text": "NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "body-override": {"text": "RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s55f298bf0048"], "status": "REASONED"}, + "multipart": {"text": "MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413.", "components": ["source", "net10"], "sources": ["source:s67f347ba7947", "net10:s792d3e8f3e42"], "status": "REASONED"}, + "iis-body": {"text": "In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s792d3e8f3e42", "net10:s529a3d7cce08"], "status": "REASONED"}, + "keepalive": {"text": "Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "header-timeout": {"text": "Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "connections": {"text": "MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "upgraded": {"text": "Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "header-count": {"text": "Kestrel MaxRequestHeaderCount defaults 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "header-size": {"text": "Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "request-rate": {"text": "MinRequestBodyDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "response-rate": {"text": "MinResponseDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "rate-scope": {"text": "HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED"}, + "h2-streams": {"text": "NET 10 HTTP/2 MaxStreamsPerConnection defaults 100.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-table": {"text": "NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-frame": {"text": "NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-header": {"text": "NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-connection-window": {"text": "NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-stream-window": {"text": "NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-ping-delay": {"text": "NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-ping-timeout": {"text": "NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-scope": {"text": "Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:sc00d8d67e93b"], "status": "REASONED"}, + "key-defaults": {"text": "Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys.", "components": ["net10"], "sources": ["net10:s66f1ab4555d6"], "status": "REASONED"}, + "key-persistence": {"text": "Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, + "key-encryption": {"text": "Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, + "key-isolation": {"text": "Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys.", "components": ["net10"], "sources": ["net10:s6682926c8da7"], "status": "REASONED"}, + "cookie-sharing": {"text": "Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration.", "components": ["net10"], "sources": ["net10:s9463626a3d0e"], "status": "REASONED"}, + "server-header": {"text": "NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access.", "components": ["source"], "sources": ["source:s18c44d6c2fb7"], "status": "REASONED"}, + "iis-headers": {"text": "Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later.", "components": ["iis", "iis-rolling"], "sources": ["iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED"}, + "openapi": {"text": "NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy.", "components": ["net10"], "sources": ["net10:s1d58219cedec"], "status": "REASONED"}, + "swagger": {"text": "Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document.", "components": ["net10"], "sources": ["net10:s1f9d4c5e975c"], "status": "REASONED"}, + "health": {"text": "Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, + "health-host": {"text": "RequireHost checks a spoofable Host header and is not a network or authentication boundary.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, + "host-filter": {"text": "AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate.", "components": ["net10"], "sources": ["net10:s5701d26de241", "net10:se101626c29d6"], "status": "REASONED"}, + "verify-redirect": {"text": "HTTP should return 307/308 with HTTPS Location.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "HTTPS must validate without -k and show HSTS.", "components": ["docs", "net10"], "sources": ["docs:s8c762260991c", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, + "verify-api": {"text": "Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first.", "components": ["net10"], "sources": ["net10:s837df4b1657f"], "status": "REASONED", "verify": [1]}, + "verify-bind": {"text": "Inspect every listener for 127.0.0.1 and ::1 behind the proxy.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, + "verify-errors": {"text": "Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED", "verify": [2]}, + "verify-body": {"text": "Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED", "verify": [3]}, + "verify-aux": {"text": "Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization.", "components": ["net10"], "sources": ["net10:s1d58219cedec", "net10:s1f9d4c5e975c", "net10:sc41b77cc06c8"], "status": "REASONED", "verify": [4]}, + "verify-migrations": {"text": "GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure.", "components": ["net10"], "sources": ["net10:s9d290374f4e8"], "status": "REASONED"}, + "verify-headers": {"text": "Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported.", "components": ["source", "iis", "iis-rolling"], "sources": ["source:s18c44d6c2fb7", "iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED", "verify": [5]}, + "verify-cookie": {"text": "Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes.", "components": ["net10"], "sources": ["net10:s837df4b1657f", "net10:s66f1ab4555d6", "net10:s9463626a3d0e"], "status": "REASONED", "verify": [6]}, + "verify-antiforgery": {"text": "Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:s66f1ab4555d6"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/frontend-frameworks.md b/plugin/skills/secure-deployment-config/references/frontend-frameworks.md index 810eed667..7c62ee6d1 100644 --- a/plugin/skills/secure-deployment-config/references/frontend-frameworks.md +++ b/plugin/skills/secure-deployment-config/references/frontend-frameworks.md @@ -1,226 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "c967988728d40cccfa0b3e543320cd6e6d4f7568099a9805fb91a941243d4c1d", - "components": { - "adapter": { - "name": "SvelteKit adapter-node", - "basis": "@sveltejs/adapter-node@5.5.7", - "sources": { - "s011f4e44c91a": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50", - "s93f13e6b8d31": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12", - "sd23cac9a730f": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60", - "s200d6e800c74": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L24-L38", - "sf17c5646623c": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L118", - "s1ffeda9bbb37": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L69", - "s8d4fa8e7426b": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L243", - "sfd51397a510e": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L17-L24", - "s6152d66c6b19": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L95-L105", - "s7b3a11637166": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L209-L239" - } - }, - "svelte": { - "name": "SvelteKit documentation", - "basis": "unknown", - "sources": { - "sc183fdb68165": "https://svelte.dev/docs/kit/$env-static-public", - "s59abf03bae6a": "https://svelte.dev/docs/kit/adapter-node", - "s329dda89ba78": "https://svelte.dev/docs/kit/configuration", - "se2fdc4fd5885": "https://svelte.dev/docs/kit/server-only-modules" - } - }, - "svelte-kit-pin": { - "name": "SvelteKit routing and hooks documentation", - "basis": "@sveltejs/kit@2.70.3", - "sources": { - "sca07fbe19546": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/10-routing.md#L291-L332", - "sa9bbf285a91c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/20-load.md#L702-L714", - "s380cc33507e0": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/30-form-actions.md#L1-L5", - "sf5b873616d3c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/30-advanced/20-hooks.md#L15-L36", - "s5ad7a525e0ea": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/60-remote-functions.md#L1300-L1303", - "s6a78ad19d884": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L70-L78", - "s0969ea98a4f1": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L178-L250", - "sffcf647d5328": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/data/index.js#L39-L97", - "sfb110948d2ca": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/client/client.js#L1217-L1235", - "s67fe32e7a18d": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L73-L106", - "sc4708064d4fd": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L111-L130", - "s95d5c7dd7a50": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L333-L343", - "sca363a35f3fc": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L456-L498", - "s89773d0c39e7": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L274-L306", - "sd1c295d2cf56": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/exports/node/index.js#L121-L160" - } - }, - "h3-pin": { - "name": "h3 request and Node adapter source at release commit", - "basis": "1.15.3", - "sources": { - "scfe5cba40964": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L275-L286", - "sdbe7e781611f": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L333-L344", - "s218970f40ae0": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/adapters/node.ts#L55-L89" - } - }, - "node-http": { - "name": "Node HTTP documentation", - "basis": "v22.22.1", - "sources": { - "sdc846082a7e3": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/http.md#L3653-L3657" - } - }, - "nuxt-lock": { - "name": "Nuxt lockfile", - "basis": "v4.0.0", - "sources": { - "se77b46c1e3ad": "https://github.com/nuxt/nuxt/blob/v4.0.0/pnpm-lock.yaml#L5204-L5205" - } - }, - "nuxt": { - "name": "Nuxt documentation", - "basis": "4.x", - "sources": { - "se2d1e4a2e433": "https://nuxt.com/docs/4.x/directory-structure/server", - "s32e447cb5ef4": "https://nuxt.com/docs/4.x/guide/going-further/runtime-config" - } - }, - "vite-pin": { - "name": "Vite bind source", - "basis": "v8.3.1", - "sources": { - "s36532f775f7f": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022", - "s8ba1c2b868b7": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/config.ts#L388-L396", - "s484948ac14b6": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/build.ts#L395" - } - }, - "vite": { - "name": "Vite documentation", - "basis": "unknown", - "sources": { - "sae9dd90908fc": "https://vite.dev/config/server-options", - "s16927b810cff": "https://vite.dev/guide/cli", - "s7c70a12e43ed": "https://vite.dev/config/build-options", - "sba6dbfdd7590": "https://vite.dev/guide/ssr" - } - }, - "grep": { - "name": "GNU grep", - "basis": "unknown", - "sources": { - "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", - "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", - "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" - } - }, - "nuxt-output": { - "name": "Nuxt output source", - "basis": "v4.0.0", - "sources": { - "sa65d64f55ca1": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L163-L170", - "se1cb3e5b4959": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L653-L659", - "sd00a53729513": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/package.json#L104" - } - }, - "nitro-output": { - "name": "Nitro output defaults", - "basis": "2.12.0", - "sources": { - "sbe9fd1f61fe0": "https://cdn.jsdelivr.net/npm/nitropack@2.12.0/dist/core/index.mjs" - } - }, - "nitro-node": { - "name": "nitropack 2.12.0 source", - "basis": "65e514d873da1d1d9edd4d9576d48d90b13e0fac", - "sources": { - "sd56c769d6dae": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/_resolve.ts#L85-L90", - "s32975278fc3f": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/preset.ts#L16-L29", - "sdfda9cd3ce7b": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/runtime/node-server.ts#L1-L23", - "sdf38c24e1f80": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/runtime/internal/app.ts#L59-L128", - "s91ab58ddc148": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/core/config/loader.ts#L134-L141", - "s0c8ca22542d2": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/package.json#L131" - } - }, - "nitro-output-docs": { - "name": "Nitro documentation (rolling)", - "basis": "unknown", - "sources": { - "s47fff9b24fba": "https://nitro.build/config", - "s51e38e411bfa": "https://nitro.build/docs/assets" - } - } - }, - "claims": { - "adapter-bind": {"text": "adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000.", "components": ["adapter", "svelte"], "sources": ["adapter:s011f4e44c91a", "adapter:s93f13e6b8d31", "adapter:sd23cac9a730f", "svelte:s59abf03bae6a"], "status": "REASONED"}, - "adapter-origin": {"text": "Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, - "adapter-ip": {"text": "ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, - "csrf-check": {"text": "csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, - "csrf-trust": {"text": "csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, - "svelte-public": {"text": "PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out.", "components": ["svelte"], "sources": ["svelte:sc183fdb68165", "svelte:s329dda89ba78"], "status": "REASONED"}, - "svelte-private": {"text": "Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds.", "components": ["svelte"], "sources": ["svelte:se2fdc4fd5885"], "status": "REASONED"}, - "svelte-routes": {"text": "Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each.", "components": ["svelte", "svelte-kit-pin"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sca07fbe19546", "svelte-kit-pin:sa9bbf285a91c", "svelte-kit-pin:s380cc33507e0", "svelte-kit-pin:sfb110948d2ca", "svelte-kit-pin:sffcf647d5328", "svelte-kit-pin:s0969ea98a4f1", "svelte-kit-pin:s6a78ad19d884"], "status": "REASONED"}, - "svelte-handle": {"text": "Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it.", "components": ["svelte", "svelte-kit-pin", "adapter"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sf5b873616d3c", "svelte-kit-pin:s5ad7a525e0ea", "svelte-kit-pin:s67fe32e7a18d", "svelte-kit-pin:sc4708064d4fd", "svelte-kit-pin:s95d5c7dd7a50", "svelte-kit-pin:sca363a35f3fc", "svelte-kit-pin:s89773d0c39e7", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b"], "status": "REASONED"}, - "nuxt-handlers": {"text": "Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, - "nuxt-auth": {"text": "Nuxt ships no built-in authentication; enforce access server-side, not with client redirects.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, - "nuxt-private": {"text": "Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, - "nuxt-public": {"text": "runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, - "vite-production": {"text": "Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform.", "components": ["vite"], "sources": ["vite:s16927b810cff"], "status": "REASONED"}, - "vite-bind": {"text": "Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests.", "components": ["vite-pin", "vite"], "sources": ["vite-pin:s36532f775f7f", "vite:sae9dd90908fc"], "status": "REASONED"}, - "vite-hosts": {"text": "server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names.", "components": ["vite"], "sources": ["vite:sae9dd90908fc"], "status": "REASONED"}, - "verify-private": {"text": "Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects.", "components": ["nuxt", "svelte"], "sources": ["nuxt:se2d1e4a2e433", "svelte:s329dda89ba78"], "status": "REASONED", "verify": [1]}, - "verify-secret": {"text": "Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root.", "components": ["grep", "adapter", "nuxt-output", "nitro-output", "vite-pin", "vite", "nitro-output-docs"], "sources": ["grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae", "adapter:s200d6e800c74", "adapter:sf17c5646623c", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b", "nuxt-output:sa65d64f55ca1", "nuxt-output:se1cb3e5b4959", "nitro-output:sbe9fd1f61fe0", "nitro-output-docs:s47fff9b24fba", "nitro-output-docs:s51e38e411bfa", "vite-pin:s8ba1c2b868b7", "vite-pin:s484948ac14b6", "vite:s7c70a12e43ed", "vite:sba6dbfdd7590"], "status": "REASONED", "verify": [1]}, - "verify-host": {"text": "The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers.", "components": ["vite", "adapter", "nitro-node", "svelte-kit-pin", "h3-pin", "node-http", "nuxt-lock"], "sources": ["vite:sae9dd90908fc", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:s0c8ca22542d2", "svelte-kit-pin:sd1c295d2cf56", "h3-pin:scfe5cba40964", "h3-pin:sdbe7e781611f", "h3-pin:s218970f40ae0", "node-http:sdc846082a7e3", "nuxt-lock:se77b46c1e3ad"], "status": "REASONED", "verify": [1]}, - "verify-proxy": {"text": "Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome.", "components": ["svelte", "nuxt", "adapter", "nuxt-output", "nitro-node"], "sources": ["svelte:s59abf03bae6a", "nuxt:s32e447cb5ef4", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nuxt-output:sd00a53729513", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:sdf38c24e1f80", "nitro-node:s91ab58ddc148", "nitro-node:s0c8ca22542d2"], "status": "REASONED", "verify": [1]} - } -} ---- # Full-stack JS frameworks: SvelteKit, Nuxt, Vite - -**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): - -- adapter: SvelteKit adapter-node (@sveltejs/adapter-node@5.5.7) -- svelte: SvelteKit documentation (unknown) -- svelte-kit-pin: SvelteKit routing and hooks documentation (@sveltejs/kit@2.70.3) -- h3-pin: h3 request and Node adapter source at release commit (1.15.3) -- node-http: Node HTTP documentation (v22.22.1) -- nuxt-lock: Nuxt lockfile (v4.0.0) -- nuxt: Nuxt documentation (4.x) -- vite-pin: Vite bind source (v8.3.1) -- vite: Vite documentation (unknown) -- grep: GNU grep (unknown) -- nuxt-output: Nuxt output source (v4.0.0) -- nitro-output: Nitro output defaults (2.12.0) -- nitro-node: nitropack 2.12.0 source (65e514d873da1d1d9edd4d9576d48d90b13e0fac) -- nitro-output-docs: Nitro documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): +Written against SvelteKit adapter-node @sveltejs/adapter-node@5.5.7, SvelteKit routing and hooks documentation @sveltejs/kit@2.70.3, Nuxt lockfile v4.0.0, Nuxt documentation 4.x, Nuxt output source v4.0.0, Vite bind source v8.3.1, and h3 request and Node adapter source at release commit 1.15.3, plus 2 more recorded bases; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- adapter-bind (REASONED; adapter, svelte): adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000. -- adapter-origin (REASONED; svelte): Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy. -- adapter-ip (REASONED; svelte): ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling. -- csrf-check (REASONED; svelte): csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks. -- csrf-trust (REASONED; svelte): csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged. -- svelte-public (REASONED; svelte): PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out. -- svelte-private (REASONED; svelte): Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds. -- svelte-routes (REASONED; svelte, svelte-kit-pin): Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each. -- svelte-handle (REASONED; svelte, svelte-kit-pin, adapter): Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it. -- nuxt-handlers (REASONED; nuxt): Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler. -- nuxt-auth (REASONED; nuxt): Nuxt ships no built-in authentication; enforce access server-side, not with client redirects. -- nuxt-private (REASONED; nuxt): Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them. -- nuxt-public (REASONED; nuxt): runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration. -- vite-production (REASONED; vite): Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform. -- vite-bind (REASONED; vite-pin, vite): Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests. -- vite-hosts (REASONED; vite): server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names. -- verify-private (REASONED; nuxt, svelte): Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects. -- verify-secret (REASONED; grep, adapter, nuxt-output, nitro-output, vite-pin, vite, nitro-output-docs): Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root. -- verify-host (REASONED; vite, adapter, nitro-node, svelte-kit-pin, h3-pin, node-http, nuxt-lock): The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers. -- verify-proxy (REASONED; svelte, nuxt, adapter, nuxt-output, nitro-node): Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome. - +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. SvelteKit, Nuxt, and Vite-based apps built by AI assistants inherit the same traps as [nextjs.md](nextjs.md): a server bind that can default wide open (SvelteKit's Node adapter binds `0.0.0.0` as of adapter-node 5.5.7, though Vite's dev server defaults to `localhost` as of Vite 8.3.1), a proxy that has to be explicitly trusted before secure cookies and correct origins work, and a public-env prefix that ships anything given it straight to the browser. Each framework also has more than one server entry point (endpoints, server routes, load functions), and a check placed in only one of them leaves the others open, exactly as with Next.js layouts versus Server Actions. @@ -384,3 +166,247 @@ REASONED: Host, proxy cookie, redirect and trusted-proxy checks per the cited Sv - Vite resolves an unset `server.host` to `'localhost'` (pinned tag v8.3.1): https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022 - SvelteKit adapter-node `host = env('HOST', '0.0.0.0')` and `port = env('PORT', !path && '3000')`, with `path = env('SOCKET_PATH', false)`, where `env()` returns a variable's value whenever it is present, even empty, and the socket-activation branch that listens on the passed descriptor instead (pinned tag @sveltejs/adapter-node@5.5.7): https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12, https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50 and https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60 - GNU grep `-f -` ("When file is '-', read patterns from standard input"), `-l` ("print the name of each input file from which output would normally have been printed") and its exit status (0 if a line is selected, 1 if none, 2 on error): https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html , https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html and https://www.gnu.org/software/grep/manual/html_node/Exit-Status.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/frontend-frameworks.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): + +- adapter: SvelteKit adapter-node (@sveltejs/adapter-node@5.5.7) +- svelte-kit-pin: SvelteKit routing and hooks documentation (@sveltejs/kit@2.70.3) +- nuxt-lock: Nuxt lockfile (v4.0.0) +- nuxt: Nuxt documentation (4.x) +- nuxt-output: Nuxt output source (v4.0.0) +- vite-pin: Vite bind source (v8.3.1) +- svelte: SvelteKit documentation (unknown) +- vite: Vite documentation (unknown) +- h3-pin: h3 request and Node adapter source at release commit (1.15.3) +- node-http: Node HTTP documentation (v22.22.1) +- nitro-output: Nitro output defaults (2.12.0) +- nitro-node: nitropack 2.12.0 source (65e514d873da1d1d9edd4d9576d48d90b13e0fac) +- nitro-output-docs: Nitro documentation (rolling) (unknown) +- grep: GNU grep (unknown) + +Claims (full source mapping in the JSON block below): + +- adapter-bind (REASONED; adapter, svelte): adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000. +- adapter-origin (REASONED; svelte): Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy. +- adapter-ip (REASONED; svelte): ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling. +- csrf-check (REASONED; svelte): csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks. +- csrf-trust (REASONED; svelte): csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged. +- svelte-public (REASONED; svelte): PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out. +- svelte-private (REASONED; svelte): Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds. +- svelte-routes (REASONED; svelte, svelte-kit-pin): Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each. +- svelte-handle (REASONED; svelte, svelte-kit-pin, adapter): Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it. +- nuxt-handlers (REASONED; nuxt): Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler. +- nuxt-auth (REASONED; nuxt): Nuxt ships no built-in authentication; enforce access server-side, not with client redirects. +- nuxt-private (REASONED; nuxt): Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them. +- nuxt-public (REASONED; nuxt): runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration. +- vite-production (REASONED; vite): Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform. +- vite-bind (REASONED; vite-pin, vite): Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests. +- vite-hosts (REASONED; vite): server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names. +- verify-private (REASONED; nuxt, svelte): Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects. +- verify-secret (REASONED; grep, adapter, nuxt-output, nitro-output, vite-pin, vite, nitro-output-docs): Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root. +- verify-host (REASONED; vite, adapter, nitro-node, svelte-kit-pin, h3-pin, node-http, nuxt-lock): The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers. +- verify-proxy (REASONED; svelte, nuxt, adapter, nuxt-output, nitro-node): Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "c967988728d40cccfa0b3e543320cd6e6d4f7568099a9805fb91a941243d4c1d", + "components": { + "adapter": { + "name": "SvelteKit adapter-node", + "basis": "@sveltejs/adapter-node@5.5.7", + "sources": { + "s011f4e44c91a": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50", + "s93f13e6b8d31": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12", + "sd23cac9a730f": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60", + "s200d6e800c74": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L24-L38", + "sf17c5646623c": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L118", + "s1ffeda9bbb37": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L69", + "s8d4fa8e7426b": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L243", + "sfd51397a510e": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L17-L24", + "s6152d66c6b19": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L95-L105", + "s7b3a11637166": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L209-L239" + } + }, + "svelte-kit-pin": { + "name": "SvelteKit routing and hooks documentation", + "basis": "@sveltejs/kit@2.70.3", + "sources": { + "sca07fbe19546": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/10-routing.md#L291-L332", + "sa9bbf285a91c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/20-load.md#L702-L714", + "s380cc33507e0": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/30-form-actions.md#L1-L5", + "sf5b873616d3c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/30-advanced/20-hooks.md#L15-L36", + "s5ad7a525e0ea": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/60-remote-functions.md#L1300-L1303", + "s6a78ad19d884": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L70-L78", + "s0969ea98a4f1": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L178-L250", + "sffcf647d5328": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/data/index.js#L39-L97", + "sfb110948d2ca": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/client/client.js#L1217-L1235", + "s67fe32e7a18d": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L73-L106", + "sc4708064d4fd": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L111-L130", + "s95d5c7dd7a50": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L333-L343", + "sca363a35f3fc": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L456-L498", + "s89773d0c39e7": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L274-L306", + "sd1c295d2cf56": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/exports/node/index.js#L121-L160" + } + }, + "nuxt-lock": { + "name": "Nuxt lockfile", + "basis": "v4.0.0", + "sources": { + "se77b46c1e3ad": "https://github.com/nuxt/nuxt/blob/v4.0.0/pnpm-lock.yaml#L5204-L5205" + } + }, + "nuxt": { + "name": "Nuxt documentation", + "basis": "4.x", + "sources": { + "se2d1e4a2e433": "https://nuxt.com/docs/4.x/directory-structure/server", + "s32e447cb5ef4": "https://nuxt.com/docs/4.x/guide/going-further/runtime-config" + } + }, + "nuxt-output": { + "name": "Nuxt output source", + "basis": "v4.0.0", + "sources": { + "sa65d64f55ca1": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L163-L170", + "se1cb3e5b4959": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L653-L659", + "sd00a53729513": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/package.json#L104" + } + }, + "vite-pin": { + "name": "Vite bind source", + "basis": "v8.3.1", + "sources": { + "s36532f775f7f": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022", + "s8ba1c2b868b7": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/config.ts#L388-L396", + "s484948ac14b6": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/build.ts#L395" + } + }, + "svelte": { + "name": "SvelteKit documentation", + "basis": "unknown", + "sources": { + "sc183fdb68165": "https://svelte.dev/docs/kit/$env-static-public", + "s59abf03bae6a": "https://svelte.dev/docs/kit/adapter-node", + "s329dda89ba78": "https://svelte.dev/docs/kit/configuration", + "se2fdc4fd5885": "https://svelte.dev/docs/kit/server-only-modules" + } + }, + "vite": { + "name": "Vite documentation", + "basis": "unknown", + "sources": { + "sae9dd90908fc": "https://vite.dev/config/server-options", + "s16927b810cff": "https://vite.dev/guide/cli", + "s7c70a12e43ed": "https://vite.dev/config/build-options", + "sba6dbfdd7590": "https://vite.dev/guide/ssr" + } + }, + "h3-pin": { + "name": "h3 request and Node adapter source at release commit", + "basis": "1.15.3", + "sources": { + "scfe5cba40964": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L275-L286", + "sdbe7e781611f": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L333-L344", + "s218970f40ae0": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/adapters/node.ts#L55-L89" + } + }, + "node-http": { + "name": "Node HTTP documentation", + "basis": "v22.22.1", + "sources": { + "sdc846082a7e3": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/http.md#L3653-L3657" + } + }, + "nitro-output": { + "name": "Nitro output defaults", + "basis": "2.12.0", + "sources": { + "sbe9fd1f61fe0": "https://cdn.jsdelivr.net/npm/nitropack@2.12.0/dist/core/index.mjs" + } + }, + "nitro-node": { + "name": "nitropack 2.12.0 source", + "basis": "65e514d873da1d1d9edd4d9576d48d90b13e0fac", + "sources": { + "sd56c769d6dae": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/_resolve.ts#L85-L90", + "s32975278fc3f": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/preset.ts#L16-L29", + "sdfda9cd3ce7b": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/runtime/node-server.ts#L1-L23", + "sdf38c24e1f80": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/runtime/internal/app.ts#L59-L128", + "s91ab58ddc148": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/core/config/loader.ts#L134-L141", + "s0c8ca22542d2": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/package.json#L131" + } + }, + "nitro-output-docs": { + "name": "Nitro documentation (rolling)", + "basis": "unknown", + "sources": { + "s47fff9b24fba": "https://nitro.build/config", + "s51e38e411bfa": "https://nitro.build/docs/assets" + } + }, + "grep": { + "name": "GNU grep", + "basis": "unknown", + "sources": { + "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", + "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", + "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" + } + } + }, + "claims": { + "adapter-bind": {"text": "adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000.", "components": ["adapter", "svelte"], "sources": ["adapter:s011f4e44c91a", "adapter:s93f13e6b8d31", "adapter:sd23cac9a730f", "svelte:s59abf03bae6a"], "status": "REASONED"}, + "adapter-origin": {"text": "Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, + "adapter-ip": {"text": "ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, + "csrf-check": {"text": "csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, + "csrf-trust": {"text": "csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, + "svelte-public": {"text": "PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out.", "components": ["svelte"], "sources": ["svelte:sc183fdb68165", "svelte:s329dda89ba78"], "status": "REASONED"}, + "svelte-private": {"text": "Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds.", "components": ["svelte"], "sources": ["svelte:se2fdc4fd5885"], "status": "REASONED"}, + "svelte-routes": {"text": "Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each.", "components": ["svelte", "svelte-kit-pin"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sca07fbe19546", "svelte-kit-pin:sa9bbf285a91c", "svelte-kit-pin:s380cc33507e0", "svelte-kit-pin:sfb110948d2ca", "svelte-kit-pin:sffcf647d5328", "svelte-kit-pin:s0969ea98a4f1", "svelte-kit-pin:s6a78ad19d884"], "status": "REASONED"}, + "svelte-handle": {"text": "Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it.", "components": ["svelte", "svelte-kit-pin", "adapter"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sf5b873616d3c", "svelte-kit-pin:s5ad7a525e0ea", "svelte-kit-pin:s67fe32e7a18d", "svelte-kit-pin:sc4708064d4fd", "svelte-kit-pin:s95d5c7dd7a50", "svelte-kit-pin:sca363a35f3fc", "svelte-kit-pin:s89773d0c39e7", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b"], "status": "REASONED"}, + "nuxt-handlers": {"text": "Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, + "nuxt-auth": {"text": "Nuxt ships no built-in authentication; enforce access server-side, not with client redirects.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, + "nuxt-private": {"text": "Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, + "nuxt-public": {"text": "runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, + "vite-production": {"text": "Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform.", "components": ["vite"], "sources": ["vite:s16927b810cff"], "status": "REASONED"}, + "vite-bind": {"text": "Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests.", "components": ["vite-pin", "vite"], "sources": ["vite-pin:s36532f775f7f", "vite:sae9dd90908fc"], "status": "REASONED"}, + "vite-hosts": {"text": "server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names.", "components": ["vite"], "sources": ["vite:sae9dd90908fc"], "status": "REASONED"}, + "verify-private": {"text": "Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects.", "components": ["nuxt", "svelte"], "sources": ["nuxt:se2d1e4a2e433", "svelte:s329dda89ba78"], "status": "REASONED", "verify": [1]}, + "verify-secret": {"text": "Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root.", "components": ["grep", "adapter", "nuxt-output", "nitro-output", "vite-pin", "vite", "nitro-output-docs"], "sources": ["grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae", "adapter:s200d6e800c74", "adapter:sf17c5646623c", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b", "nuxt-output:sa65d64f55ca1", "nuxt-output:se1cb3e5b4959", "nitro-output:sbe9fd1f61fe0", "nitro-output-docs:s47fff9b24fba", "nitro-output-docs:s51e38e411bfa", "vite-pin:s8ba1c2b868b7", "vite-pin:s484948ac14b6", "vite:s7c70a12e43ed", "vite:sba6dbfdd7590"], "status": "REASONED", "verify": [1]}, + "verify-host": {"text": "The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers.", "components": ["vite", "adapter", "nitro-node", "svelte-kit-pin", "h3-pin", "node-http", "nuxt-lock"], "sources": ["vite:sae9dd90908fc", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:s0c8ca22542d2", "svelte-kit-pin:sd1c295d2cf56", "h3-pin:scfe5cba40964", "h3-pin:sdbe7e781611f", "h3-pin:s218970f40ae0", "node-http:sdc846082a7e3", "nuxt-lock:se77b46c1e3ad"], "status": "REASONED", "verify": [1]}, + "verify-proxy": {"text": "Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome.", "components": ["svelte", "nuxt", "adapter", "nuxt-output", "nitro-node"], "sources": ["svelte:s59abf03bae6a", "nuxt:s32e447cb5ef4", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nuxt-output:sd00a53729513", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:sdf38c24e1f80", "nitro-node:s91ab58ddc148", "nitro-node:s0c8ca22542d2"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/gpu-clouds.md b/plugin/skills/secure-deployment-config/references/gpu-clouds.md index 57411011e..5c857761a 100644 --- a/plugin/skills/secure-deployment-config/references/gpu-clouds.md +++ b/plugin/skills/secure-deployment-config/references/gpu-clouds.md @@ -1,225 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "7dc59d278092451cbf9bc33836b92a411a629f5285413f75b1409ecbf11a9774", - "components": { - "runpod": { - "name": "RunPod documentation", - "basis": "unknown", - "sources": { - "s817198f039ee": "https://docs.runpod.io/pods/configuration/expose-ports", - "s13c43b856d4f": "https://docs.runpod.io/pods/configuration/use-ssh" - } - }, - "vast": { - "name": "Vast.ai documentation", - "basis": "unknown", - "sources": { - "sc25989a1613e": "https://docs.vast.ai/guides/instances/connect/networking", - "s0bdbda534723": "https://docs.vast.ai/guides/instances/connect/instance-portal", - "s6e0303ef19fe": "https://docs.vast.ai/guides/instances/connect/ssh" - } - }, - "vast-image": { - "name": "Vast.ai base image", - "basis": "00064421641881c1f83ff58c55f13cbc10cbea4d", - "sources": { - "sf75c3c07819e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md", - "s5d2b91002bac": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L175-L187", - "s229c9334547e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L135-L147", - "s3b735c4b3632": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L154-L158", - "s037a7cbd778c": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/instance-tools/tests/base/26-caddy-auth.sh#L97-L110", - "sdc669faa429d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/caddy.sh#L9-L14", - "s0428d55e9145": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24", - "s1b8c6c46f358": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50", - "s44f3439f13de": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80", - "sd0ba21945c9a": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L14-L40", - "sc77a7717bbc4": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L68-L71", - "s3c43b58f9e41": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L75-L93", - "sf1b910a375c6": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L125-L132", - "sae1085635ff9": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L175-L186", - "sd9c32e411f08": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L212-L240", - "s5a43e0cf8348": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L293-L308", - "sea35d951abe5": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L311-L321", - "s4a05112af859": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L323-L387", - "s5c18e60bb9e7": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L400-L413", - "s92be3bd5cdef": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/launch.sh#L77-L92", - "s7ac7955e604d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/syncthing.sh#L14-L16", - "s10acc068e127": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/etc/vast_boot.d/10-prep-env.sh#L12-L27", - "s6d0bc9f2a0da": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/capabilities/manifest.py#L674-L694" - } - }, - "lambda": { - "name": "Lambda Public Cloud documentation", - "basis": "unknown", - "sources": { - "se22064748974": "https://docs.lambda.ai/public-cloud/firewalls/", - "sb12b4a953833": "https://docs.lambda.ai/public-cloud/on-demand/connecting-instance/" - } - }, - "modal": { - "name": "Modal proxy authentication", - "basis": "unknown", - "sources": { - "s7d083f819a21": "https://modal.com/docs/guide/webhook-proxy-auth", - "s12a4f5e04dbe": "https://modal.com/docs/guide/shared-endpoints", - "s33e1c2c2faf3": "https://modal.com/docs/guide/endpoints" - } - }, - "jupyter": { - "name": "Jupyter Server security", - "basis": "unknown", - "sources": { - "sc703a8030c8f": "https://jupyter-server.readthedocs.io/en/latest/operators/security.html" - } - }, - "jupyter-contents": { - "name": "Jupyter Server contents authentication", - "basis": "v2.18.0", - "sources": { - "sdc3ad9152505": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L83-L186", - "sf0a3ea8ee42e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L436-L445", - "s4bdc7b9183f3": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L782-L789", - "s4e7504ed1ac8": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L1201", - "s52316685e1e4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L465-L516", - "s83f1081e863e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L255-L298", - "s6c884bdf8fb4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L6-L26", - "sb7fab0e85c94": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L315-L322", - "s58a8a77857b5": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/api/api.yaml#L82-L137" - } - }, - "iproute2": { - "name": "iproute2 ss manual", - "basis": "v6.12.0", - "sources": { - "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", - "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", - "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", - "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375", - "s6c87acff18cf": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L358-L359", - "sf8fdfa16eb3f": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L6114-L6117", - "s1e96518d2ede": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L4235-L4240" - } - }, - "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" - } - }, - "ollama": { - "name": "Ollama", - "basis": "v0.34.2", - "sources": { - "s96f77aee8bdd": "https://github.com/ollama/ollama/blob/v0.34.2/server/routes.go", - "sd146dc5d917d": "https://github.com/ollama/ollama/blob/v0.34.2/api/client.go", - "s7478dad099ab": "https://github.com/ollama/ollama/blob/v0.34.2/cmd/cmd.go", - "s2abe15b73bce": "https://github.com/ollama/ollama/blob/v0.34.2/envconfig/config.go" - } - } - }, - "claims": { - "publication": {"text": "Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover.", "components": ["runpod", "vast", "lambda", "modal"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "lambda:se22064748974", "modal:s7d083f819a21"], "status": "REASONED"}, - "runpod-http": {"text": "Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "runpod-tcp": {"text": "Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "runpod-symmetry": {"text": "TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "vast-ssh-port": {"text": "Vast.ai SSH launch mode opens internal port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, - "vast-jupyter-port": {"text": "Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, - "vast-mapping": {"text": "Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, - "vast-proxy": {"text": "Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, - "vast-token": {"text": "Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, - "vast-auth-controls": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results.", "components": ["vast-image", "vast"], "sources": ["vast-image:sf75c3c07819e", "vast:s0bdbda534723", "vast-image:s5d2b91002bac", "vast-image:s229c9334547e", "vast-image:s3b735c4b3632", "vast-image:s037a7cbd778c", "vast-image:sdc669faa429d", "vast-image:sc77a7717bbc4", "vast-image:s3c43b58f9e41", "vast-image:sf1b910a375c6", "vast-image:sd9c32e411f08", "vast-image:sea35d951abe5", "vast-image:s4a05112af859"], "status": "REASONED"}, - "vast-portal-routing": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential.", "components": ["vast-image"], "sources": ["vast-image:sd0ba21945c9a", "vast-image:sd9c32e411f08"], "status": "REASONED"}, - "vast-portal-jupyter": {"text": "Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication.", "components": ["vast-image"], "sources": ["vast-image:s10acc068e127", "vast-image:sd9c32e411f08", "vast-image:sd0ba21945c9a"], "status": "REASONED"}, - "vast-auth-defaults": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set.", "components": ["vast-image"], "sources": ["vast-image:s3c43b58f9e41"], "status": "REASONED"}, - "vast-auth-routes": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD.", "components": ["vast-image"], "sources": ["vast-image:sf1b910a375c6", "vast-image:s4a05112af859", "vast-image:sea35d951abe5"], "status": "REASONED"}, - "vast-auth-exemptions": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there.", "components": ["vast-image"], "sources": ["vast-image:sc77a7717bbc4", "vast-image:sd9c32e411f08", "vast-image:s5a43e0cf8348", "vast-image:sae1085635ff9", "vast-image:s4a05112af859"], "status": "REASONED"}, - "vast-token-reuse": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too.", "components": ["vast-image"], "sources": ["vast-image:s5c18e60bb9e7", "vast-image:s92be3bd5cdef", "vast-image:s7ac7955e604d"], "status": "REASONED"}, - "vast-manifest-report": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to \"true\": reporting tests equality to \"true\", enforcement inequality to \"false\". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce.", "components": ["vast-image"], "sources": ["vast-image:s6d0bc9f2a0da", "vast-image:s3c43b58f9e41"], "status": "REASONED"}, - "lambda-default": {"text": "Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, - "lambda-rule": {"text": "An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, - "modal-web": {"text": "Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "modal-dedicated": {"text": "Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "modal-server": {"text": "Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "modal-shared": {"text": "Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public.", "components": ["modal"], "sources": ["modal:s7d083f819a21", "modal:s12a4f5e04dbe", "modal:s33e1c2c2faf3"], "status": "REASONED"}, - "modal-credentials": {"text": "Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer .; missing credentials on a protected endpoint return 401.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, - "runpod-bind": {"text": "RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, - "listener-auth": {"text": "Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication.", "components": ["runpod", "vast", "jupyter", "ollama"], "sources": ["runpod:s817198f039ee", "vast:s0bdbda534723", "jupyter:sc703a8030c8f", "ollama:s96f77aee8bdd", "ollama:sd146dc5d917d", "ollama:s7478dad099ab", "ollama:s2abe15b73bce"], "status": "REASONED"}, - "mfa": {"text": "MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login.", "components": ["modal", "jupyter"], "sources": ["modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED"}, - "lambda-ssh": {"text": "Lambda requires an SSH key at launch; keep the private key off the instance.", "components": ["lambda"], "sources": ["lambda:sb12b4a953833"], "status": "REASONED"}, - "vast-ssh": {"text": "Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance.", "components": ["vast"], "sources": ["vast:s6e0303ef19fe"], "status": "REASONED"}, - "runpod-ssh": {"text": "RunPod recommends SSH keys and offers an optional password; use the key and skip the password.", "components": ["runpod"], "sources": ["runpod:s13c43b856d4f"], "status": "REASONED"}, - "verify-inventory": {"text": "This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside.", "components": ["runpod", "vast", "iproute2", "network-namespaces"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850", "iproute2:s6c87acff18cf", "iproute2:sf8fdfa16eb3f", "iproute2:s1e96518d2ede", "network-namespaces:s427c923e346f"], "status": "REASONED", "verify": [1]}, - "verify-jupyter": {"text": "This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls.", "components": ["jupyter", "jupyter-contents"], "sources": ["jupyter:sc703a8030c8f", "jupyter-contents:sdc3ad9152505", "jupyter-contents:sf0a3ea8ee42e", "jupyter-contents:s4bdc7b9183f3", "jupyter-contents:s4e7504ed1ac8", "jupyter-contents:s52316685e1e4", "jupyter-contents:s83f1081e863e", "jupyter-contents:s6c884bdf8fb4", "jupyter-contents:sb7fab0e85c94", "jupyter-contents:s58a8a77857b5"], "status": "REASONED", "verify": [1]}, - "verify-mapping": {"text": "Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control.", "components": ["runpod", "vast", "modal", "jupyter"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED", "verify": [1]}, - "verify-portal-exempt": {"text": "For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE.", "components": ["vast-image"], "sources": ["vast-image:sae1085635ff9", "vast-image:s4a05112af859", "vast-image:sd9c32e411f08", "vast-image:s3c43b58f9e41"], "status": "REASONED", "verify": [2]}, - "remote-desktop": {"text": "Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it.", "components": ["runpod", "vast", "vast-image"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast-image:s0428d55e9145", "vast-image:s1b8c6c46f358", "vast-image:s44f3439f13de"], "status": "REASONED"} - } -} ---- # Rented GPUs: RunPod, Vast.ai, Lambda, Modal - -**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. +Written against RunPod documentation (no recorded version), Jupyter Server contents authentication v2.18.0, Ollama v0.34.2, iproute2 ss manual v6.12.0, and Linux network namespaces manual man-pages-5.13; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 35 claims, all reasoned from cited sources. -Components (recorded basis): - -- runpod: RunPod documentation (unknown) -- vast: Vast.ai documentation (unknown) -- vast-image: Vast.ai base image (00064421641881c1f83ff58c55f13cbc10cbea4d) -- lambda: Lambda Public Cloud documentation (unknown) -- modal: Modal proxy authentication (unknown) -- jupyter: Jupyter Server security (unknown) -- jupyter-contents: Jupyter Server contents authentication (v2.18.0) -- iproute2: iproute2 ss manual (v6.12.0) -- network-namespaces: Linux network namespaces manual (man-pages-5.13) -- ollama: Ollama (v0.34.2) - -Claims (full source mapping in the front matter): - -- publication (REASONED; runpod, vast, lambda, modal): Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover. -- runpod-http (REASONED; runpod): Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication. -- runpod-tcp (REASONED; runpod): Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data. -- runpod-symmetry (REASONED; runpod): TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public. -- vast-ssh-port (REASONED; vast): Vast.ai SSH launch mode opens internal port 22 by default. -- vast-jupyter-port (REASONED; vast): Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default. -- vast-mapping (REASONED; vast): Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step. -- vast-proxy (REASONED; vast): Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation. -- vast-token (REASONED; vast): Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates. -- vast-auth-controls (REASONED; vast-image, vast): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results. -- vast-portal-routing (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential. -- vast-portal-jupyter (REASONED; vast-image): Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication. -- vast-auth-defaults (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set. -- vast-auth-routes (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD. -- vast-auth-exemptions (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there. -- vast-token-reuse (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too. -- vast-manifest-report (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to "true": reporting tests equality to "true", enforcement inequality to "false". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce. -- lambda-default (REASONED; lambda): Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch. -- lambda-rule (REASONED; lambda): An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access. -- modal-web (REASONED; modal): Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication. -- modal-dedicated (REASONED; modal): Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public. -- modal-server (REASONED; modal): Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public. -- modal-shared (REASONED; modal): Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public. -- modal-credentials (REASONED; modal): Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer <token_id>.<token_secret>; missing credentials on a protected endpoint return 401. -- runpod-bind (REASONED; runpod): RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost. -- listener-auth (REASONED; runpod, vast, jupyter, ollama): Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication. -- mfa (REASONED; modal, jupyter): MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login. -- lambda-ssh (REASONED; lambda): Lambda requires an SSH key at launch; keep the private key off the instance. -- vast-ssh (REASONED; vast): Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance. -- runpod-ssh (REASONED; runpod): RunPod recommends SSH keys and offers an optional password; use the key and skip the password. -- verify-inventory (REASONED; runpod, vast, iproute2, network-namespaces): This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside. -- verify-jupyter (REASONED; jupyter, jupyter-contents): This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls. -- verify-mapping (REASONED; runpod, vast, modal, jupyter): Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control. -- verify-portal-exempt (REASONED; vast-image): For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE. -- remote-desktop (REASONED; runpod, vast, vast-image): Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it. - +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. Unlike the major clouds, most rented-GPU platforms put no default-deny firewall in front of the box. How a bound port becomes public differs by platform: on RunPod you expose a port (HTTP or TCP), on Vast.ai a launch mode or an added port maps it to a public port, on Lambda you open a firewall rule (its inbound firewall is deny-by-default), and on Modal it depends on the serverless construct. Raw port publication and a firewall allowance add NO authentication of their own, so a listener exposed that way is reachable with only whatever auth it has itself; an authenticating platform proxy is the exception (Modal's Endpoints and Servers require proxy auth by default and Shared Endpoints always do, only a public Web endpoint does not, and Vast.ai's Instance Portal gates the routes it proxies), but do not assume one is in front of a given port. Treat every listener the same way you would on your own hardware: bind it privately or authenticate it, per [ollama.md](ollama.md) and [model-servers.md](model-servers.md). @@ -424,3 +207,246 @@ REASONED: following block; this guide's test design for the Vast.ai portal's pat - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop x11vnc port and password fallback; does not establish password strength (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24 - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop browser and direct connection choices (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50 - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop documented VNC password and override (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80 + + + +## 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/gpu-clouds.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): + +- runpod: RunPod documentation (unknown) +- vast: Vast.ai documentation (unknown) +- vast-image: Vast.ai base image (00064421641881c1f83ff58c55f13cbc10cbea4d) +- lambda: Lambda Public Cloud documentation (unknown) +- modal: Modal proxy authentication (unknown) +- jupyter: Jupyter Server security (unknown) +- jupyter-contents: Jupyter Server contents authentication (v2.18.0) +- ollama: Ollama (v0.34.2) +- iproute2: iproute2 ss manual (v6.12.0) +- network-namespaces: Linux network namespaces manual (man-pages-5.13) + +Claims (full source mapping in the JSON block below): + +- publication (REASONED; runpod, vast, lambda, modal): Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover. +- runpod-http (REASONED; runpod): Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication. +- runpod-tcp (REASONED; runpod): Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data. +- runpod-symmetry (REASONED; runpod): TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public. +- vast-ssh-port (REASONED; vast): Vast.ai SSH launch mode opens internal port 22 by default. +- vast-jupyter-port (REASONED; vast): Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default. +- vast-mapping (REASONED; vast): Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step. +- vast-proxy (REASONED; vast): Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation. +- vast-token (REASONED; vast): Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates. +- vast-auth-controls (REASONED; vast-image, vast): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results. +- vast-portal-routing (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential. +- vast-portal-jupyter (REASONED; vast-image): Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication. +- vast-auth-defaults (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set. +- vast-auth-routes (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD. +- vast-auth-exemptions (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there. +- vast-token-reuse (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too. +- vast-manifest-report (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to "true": reporting tests equality to "true", enforcement inequality to "false". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce. +- lambda-default (REASONED; lambda): Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch. +- lambda-rule (REASONED; lambda): An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access. +- modal-web (REASONED; modal): Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication. +- modal-dedicated (REASONED; modal): Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public. +- modal-server (REASONED; modal): Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public. +- modal-shared (REASONED; modal): Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public. +- modal-credentials (REASONED; modal): Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer <token_id>.<token_secret>; missing credentials on a protected endpoint return 401. +- runpod-bind (REASONED; runpod): RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost. +- listener-auth (REASONED; runpod, vast, jupyter, ollama): Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication. +- mfa (REASONED; modal, jupyter): MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login. +- lambda-ssh (REASONED; lambda): Lambda requires an SSH key at launch; keep the private key off the instance. +- vast-ssh (REASONED; vast): Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance. +- runpod-ssh (REASONED; runpod): RunPod recommends SSH keys and offers an optional password; use the key and skip the password. +- verify-inventory (REASONED; runpod, vast, iproute2, network-namespaces): This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside. +- verify-jupyter (REASONED; jupyter, jupyter-contents): This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls. +- verify-mapping (REASONED; runpod, vast, modal, jupyter): Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control. +- verify-portal-exempt (REASONED; vast-image): For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE. +- remote-desktop (REASONED; runpod, vast, vast-image): Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "7dc59d278092451cbf9bc33836b92a411a629f5285413f75b1409ecbf11a9774", + "components": { + "runpod": { + "name": "RunPod documentation", + "basis": "unknown", + "sources": { + "s817198f039ee": "https://docs.runpod.io/pods/configuration/expose-ports", + "s13c43b856d4f": "https://docs.runpod.io/pods/configuration/use-ssh" + } + }, + "vast": { + "name": "Vast.ai documentation", + "basis": "unknown", + "sources": { + "sc25989a1613e": "https://docs.vast.ai/guides/instances/connect/networking", + "s0bdbda534723": "https://docs.vast.ai/guides/instances/connect/instance-portal", + "s6e0303ef19fe": "https://docs.vast.ai/guides/instances/connect/ssh" + } + }, + "vast-image": { + "name": "Vast.ai base image", + "basis": "00064421641881c1f83ff58c55f13cbc10cbea4d", + "sources": { + "sf75c3c07819e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md", + "s5d2b91002bac": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L175-L187", + "s229c9334547e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L135-L147", + "s3b735c4b3632": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L154-L158", + "s037a7cbd778c": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/instance-tools/tests/base/26-caddy-auth.sh#L97-L110", + "sdc669faa429d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/caddy.sh#L9-L14", + "s0428d55e9145": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24", + "s1b8c6c46f358": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50", + "s44f3439f13de": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80", + "sd0ba21945c9a": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L14-L40", + "sc77a7717bbc4": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L68-L71", + "s3c43b58f9e41": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L75-L93", + "sf1b910a375c6": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L125-L132", + "sae1085635ff9": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L175-L186", + "sd9c32e411f08": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L212-L240", + "s5a43e0cf8348": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L293-L308", + "sea35d951abe5": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L311-L321", + "s4a05112af859": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L323-L387", + "s5c18e60bb9e7": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L400-L413", + "s92be3bd5cdef": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/launch.sh#L77-L92", + "s7ac7955e604d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/syncthing.sh#L14-L16", + "s10acc068e127": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/etc/vast_boot.d/10-prep-env.sh#L12-L27", + "s6d0bc9f2a0da": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/capabilities/manifest.py#L674-L694" + } + }, + "lambda": { + "name": "Lambda Public Cloud documentation", + "basis": "unknown", + "sources": { + "se22064748974": "https://docs.lambda.ai/public-cloud/firewalls/", + "sb12b4a953833": "https://docs.lambda.ai/public-cloud/on-demand/connecting-instance/" + } + }, + "modal": { + "name": "Modal proxy authentication", + "basis": "unknown", + "sources": { + "s7d083f819a21": "https://modal.com/docs/guide/webhook-proxy-auth", + "s12a4f5e04dbe": "https://modal.com/docs/guide/shared-endpoints", + "s33e1c2c2faf3": "https://modal.com/docs/guide/endpoints" + } + }, + "jupyter": { + "name": "Jupyter Server security", + "basis": "unknown", + "sources": { + "sc703a8030c8f": "https://jupyter-server.readthedocs.io/en/latest/operators/security.html" + } + }, + "jupyter-contents": { + "name": "Jupyter Server contents authentication", + "basis": "v2.18.0", + "sources": { + "sdc3ad9152505": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L83-L186", + "sf0a3ea8ee42e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L436-L445", + "s4bdc7b9183f3": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L782-L789", + "s4e7504ed1ac8": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L1201", + "s52316685e1e4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L465-L516", + "s83f1081e863e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L255-L298", + "s6c884bdf8fb4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L6-L26", + "sb7fab0e85c94": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L315-L322", + "s58a8a77857b5": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/api/api.yaml#L82-L137" + } + }, + "ollama": { + "name": "Ollama", + "basis": "v0.34.2", + "sources": { + "s96f77aee8bdd": "https://github.com/ollama/ollama/blob/v0.34.2/server/routes.go", + "sd146dc5d917d": "https://github.com/ollama/ollama/blob/v0.34.2/api/client.go", + "s7478dad099ab": "https://github.com/ollama/ollama/blob/v0.34.2/cmd/cmd.go", + "s2abe15b73bce": "https://github.com/ollama/ollama/blob/v0.34.2/envconfig/config.go" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.12.0", + "sources": { + "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", + "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", + "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", + "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375", + "s6c87acff18cf": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L358-L359", + "sf8fdfa16eb3f": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L6114-L6117", + "s1e96518d2ede": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L4235-L4240" + } + }, + "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": { + "publication": {"text": "Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover.", "components": ["runpod", "vast", "lambda", "modal"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "lambda:se22064748974", "modal:s7d083f819a21"], "status": "REASONED"}, + "runpod-http": {"text": "Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "runpod-tcp": {"text": "Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "runpod-symmetry": {"text": "TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "vast-ssh-port": {"text": "Vast.ai SSH launch mode opens internal port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-jupyter-port": {"text": "Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-mapping": {"text": "Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-proxy": {"text": "Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, + "vast-token": {"text": "Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, + "vast-auth-controls": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results.", "components": ["vast-image", "vast"], "sources": ["vast-image:sf75c3c07819e", "vast:s0bdbda534723", "vast-image:s5d2b91002bac", "vast-image:s229c9334547e", "vast-image:s3b735c4b3632", "vast-image:s037a7cbd778c", "vast-image:sdc669faa429d", "vast-image:sc77a7717bbc4", "vast-image:s3c43b58f9e41", "vast-image:sf1b910a375c6", "vast-image:sd9c32e411f08", "vast-image:sea35d951abe5", "vast-image:s4a05112af859"], "status": "REASONED"}, + "vast-portal-routing": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential.", "components": ["vast-image"], "sources": ["vast-image:sd0ba21945c9a", "vast-image:sd9c32e411f08"], "status": "REASONED"}, + "vast-portal-jupyter": {"text": "Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication.", "components": ["vast-image"], "sources": ["vast-image:s10acc068e127", "vast-image:sd9c32e411f08", "vast-image:sd0ba21945c9a"], "status": "REASONED"}, + "vast-auth-defaults": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set.", "components": ["vast-image"], "sources": ["vast-image:s3c43b58f9e41"], "status": "REASONED"}, + "vast-auth-routes": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD.", "components": ["vast-image"], "sources": ["vast-image:sf1b910a375c6", "vast-image:s4a05112af859", "vast-image:sea35d951abe5"], "status": "REASONED"}, + "vast-auth-exemptions": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there.", "components": ["vast-image"], "sources": ["vast-image:sc77a7717bbc4", "vast-image:sd9c32e411f08", "vast-image:s5a43e0cf8348", "vast-image:sae1085635ff9", "vast-image:s4a05112af859"], "status": "REASONED"}, + "vast-token-reuse": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too.", "components": ["vast-image"], "sources": ["vast-image:s5c18e60bb9e7", "vast-image:s92be3bd5cdef", "vast-image:s7ac7955e604d"], "status": "REASONED"}, + "vast-manifest-report": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to \"true\": reporting tests equality to \"true\", enforcement inequality to \"false\". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce.", "components": ["vast-image"], "sources": ["vast-image:s6d0bc9f2a0da", "vast-image:s3c43b58f9e41"], "status": "REASONED"}, + "lambda-default": {"text": "Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, + "lambda-rule": {"text": "An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, + "modal-web": {"text": "Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-dedicated": {"text": "Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-server": {"text": "Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-shared": {"text": "Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public.", "components": ["modal"], "sources": ["modal:s7d083f819a21", "modal:s12a4f5e04dbe", "modal:s33e1c2c2faf3"], "status": "REASONED"}, + "modal-credentials": {"text": "Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer .; missing credentials on a protected endpoint return 401.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "runpod-bind": {"text": "RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "listener-auth": {"text": "Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication.", "components": ["runpod", "vast", "jupyter", "ollama"], "sources": ["runpod:s817198f039ee", "vast:s0bdbda534723", "jupyter:sc703a8030c8f", "ollama:s96f77aee8bdd", "ollama:sd146dc5d917d", "ollama:s7478dad099ab", "ollama:s2abe15b73bce"], "status": "REASONED"}, + "mfa": {"text": "MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login.", "components": ["modal", "jupyter"], "sources": ["modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED"}, + "lambda-ssh": {"text": "Lambda requires an SSH key at launch; keep the private key off the instance.", "components": ["lambda"], "sources": ["lambda:sb12b4a953833"], "status": "REASONED"}, + "vast-ssh": {"text": "Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance.", "components": ["vast"], "sources": ["vast:s6e0303ef19fe"], "status": "REASONED"}, + "runpod-ssh": {"text": "RunPod recommends SSH keys and offers an optional password; use the key and skip the password.", "components": ["runpod"], "sources": ["runpod:s13c43b856d4f"], "status": "REASONED"}, + "verify-inventory": {"text": "This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside.", "components": ["runpod", "vast", "iproute2", "network-namespaces"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850", "iproute2:s6c87acff18cf", "iproute2:sf8fdfa16eb3f", "iproute2:s1e96518d2ede", "network-namespaces:s427c923e346f"], "status": "REASONED", "verify": [1]}, + "verify-jupyter": {"text": "This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls.", "components": ["jupyter", "jupyter-contents"], "sources": ["jupyter:sc703a8030c8f", "jupyter-contents:sdc3ad9152505", "jupyter-contents:sf0a3ea8ee42e", "jupyter-contents:s4bdc7b9183f3", "jupyter-contents:s4e7504ed1ac8", "jupyter-contents:s52316685e1e4", "jupyter-contents:s83f1081e863e", "jupyter-contents:s6c884bdf8fb4", "jupyter-contents:sb7fab0e85c94", "jupyter-contents:s58a8a77857b5"], "status": "REASONED", "verify": [1]}, + "verify-mapping": {"text": "Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control.", "components": ["runpod", "vast", "modal", "jupyter"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED", "verify": [1]}, + "verify-portal-exempt": {"text": "For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE.", "components": ["vast-image"], "sources": ["vast-image:sae1085635ff9", "vast-image:s4a05112af859", "vast-image:sd9c32e411f08", "vast-image:s3c43b58f9e41"], "status": "REASONED", "verify": [2]}, + "remote-desktop": {"text": "Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it.", "components": ["runpod", "vast", "vast-image"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast-image:s0428d55e9145", "vast-image:s1b8c6c46f358", "vast-image:s44f3439f13de"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/low-code-builders.md b/plugin/skills/secure-deployment-config/references/low-code-builders.md index fa80f84f5..380bf29ed 100644 --- a/plugin/skills/secure-deployment-config/references/low-code-builders.md +++ b/plugin/skills/secure-deployment-config/references/low-code-builders.md @@ -1,305 +1,32 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "43cf22968b497a23408d386c38d01f763e391f115cf9a92f4fb443ce28cea9a8", - "components": { - "nocodb": { - "name": "NocoDB", - "basis": "2026.09.0", - "sources": { - "sa19c6557b712": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/Noco.ts", - "s635b35ce4689": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/trustProxy.ts", - "sd74ca4df55b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/run/dockerEntry.ts", - "s5ea55dc5f0d9": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/services/users/users.service.ts", - "sd0e9f451673f": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/interface/AppSettings.ts", - "s8a1de0bc97b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initAdminFromEnv.ts", - "sca40040b40cc": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/encryptDecrypt.ts", - "saf056bb4ff37": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initDataSourceEncryption.ts", - "sbfbf630008e1": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start.sh", - "se5da9707c684": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start-litestream.sh", - "s536899ecd4a5": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md", - "sca0c9c0ecf24": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L56-L75", - "sb4d5b659f7b8": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L77-L90", - "s2769b9054778": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L82", - "s2b16e2c93606": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L90", - "s118384fa1d9f": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L95", - "s9bee4adc612e": "https://api.github.com/repos/nocodb/nocodb/releases/tags/2026.09.0", - "s3f296c702bc5": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/package.json#L3", - "se81c30b3f43b": "https://github.com/nocodb/nocodb/blob/2026.09.0/docker-compose/examples/external-postgres-and-redis/docker.env" - } - }, - "docker": { - "name": "Docker docs", - "basis": "unknown", - "sources": { - "sd34e76a09f6c": "https://docs.docker.com/reference/cli/docker/container/exec/", - "s2fecb6db5480": "https://docs.docker.com/engine/network/firewall-iptables/" - } - }, - "docker-new": { - "name": "Docker CLI", - "basis": "v27.2.0", - "sources": { - "sca9a0e1e4b6a": "https://github.com/docker/cli/blob/v27.2.0/cli/command/formatter/container.go" - } - }, - "docker-old": { - "name": "Docker CLI", - "basis": "v27.1.2", - "sources": { - "sa202ad11d4ed": "https://github.com/docker/cli/blob/v27.1.2/cli/command/formatter/container.go" - } - }, - "node": { - "name": "Node.js", - "basis": "v22.22.1", - "sources": { - "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" - } - }, - "baserow": { - "name": "Baserow", - "basis": "2.3.4", - "sources": { - "s1b31314982f4": "https://github.com/baserow/baserow/blob/2.3.4/docker-compose.yml", - "s3f61036aca03": "https://github.com/baserow/baserow/blob/2.3.4/docs/installation/install-with-docker.md", - "s34b87f60dcc6": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/user/handler.py", - "s4588448964a2": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/models.py", - "sd858f9ee45e0": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/apps.py", - "s1a472d828699": "https://github.com/baserow/baserow/blob/2.3.4/.env.example" - } - }, - "appsmith": { - "name": "Appsmith", - "basis": "v2.4.1", - "sources": { - "s2650629933a1": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/resources/application-ce.properties", - "sd1c55be91e30": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java", - "se3cd894dc579": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java", - "sa68daf87cde7": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/fs/opt/appsmith/entrypoint.sh", - "s9fdcff5a28a4": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/docker-compose.yml", - "sfa551c344e8b": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/aws_ami/docker-compose.yml" - } - }, - "budibase": { - "name": "Budibase", - "basis": "v3.46.0", - "sources": { - "s735ead81ce9c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env", - "s74d858053d12": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml", - "s7fbf706a4c8f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf", - "s45379b35a980": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts", - "sc0a3ac85466f": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts", - "s4902d93986c7": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts", - "s5209f2549c9b": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env#L4-L16", - "s9afbeb2063b5": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L111-L120", - "s32775c45ba51": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L129-L144", - "s4d349b4d4ac3": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L86", - "s2a033b3a4117": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L236-L239", - "s2db467c4f1cf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L40-L43", - "sbd761572fef0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L133-L167", - "sb8f5a18ab4ee": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L13-L28", - "s7883460cabbf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L75-L79", - "se18a9c7182fb": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts#L229-L274", - "s816a78b83956": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/server/src/startup/index.ts#L226-L266", - "sedd5e0682bf0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/src/environment.ts#L267-L268", - "s35ae42bd7a1c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L27-L28", - "scdcd8b6e7071": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L89-L91", - "sc74f36aa8340": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L151-L154", - "sf6e2a660380f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/VERSION#L1", - "sfdc76e71aff6": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/runner.sh#L165-L166", - "s851ad716e105": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/docker-entrypoint.sh#L70-L75", - "se993218560de": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/hosting.properties#L4-L18", - "s6179ec0d9abd": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot#L11-L15", - "s9cc27222e133": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/tests/jestEnv.ts#L9-L10", - "sbe7e0469e7a8": "https://github.com/Budibase/budibase/blob/v3.46.0/scripts/dev/manage.js#L11-L12" - } - }, - "couchdb": { - "name": "CouchDB", - "basis": "3.5.2", - "sources": { - "s34292991c4ec": "https://github.com/apache/couchdb/blob/3.5.2/src/docs/src/api/server/authn.rst" - } - }, - "litellm": { - "name": "LiteLLM authentication source", - "basis": "6ef7b86748118ceecd95271727c2fa167ce55fec", - "sources": { - "sbe6fa0b81017": "https://raw.githubusercontent.com/BerriAI/litellm/6ef7b86748118ceecd95271727c2fa167ce55fec/litellm/proxy/auth/user_api_key_auth.py" - } - }, - "windmill": { - "name": "Windmill", - "basis": "v1.817.0", - "sources": { - "s91439e21d4ac": "https://github.com/windmill-labs/windmill/blob/v1.817.0/README.md", - "s8e4bbba271f2": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/src/main.rs", - "sd775b7c95872": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs", - "sfcc72cf06c9e": "https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml", - "s345ca635c837": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql", - "s03c33b490c08": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql" - } - } - }, - "claims": { - "private": {"text": "Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials.", "components": ["nocodb", "baserow", "appsmith", "budibase", "windmill"], "sources": ["nocodb:sd74ca4df55b0", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "nocodb-bind": {"text": "PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall.", "components": ["nocodb", "node"], "sources": ["nocodb:sa19c6557b712", "nocodb:sd74ca4df55b0", "node:sfb3e306c102d"], "status": "REASONED"}, - "nocodb-first": {"text": "First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:s8a1de0bc97b0"], "status": "REASONED"}, - "nocodb-signup": {"text": "invite_only_signup defaults false; enable to stop later self-signups.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, - "nocodb-encryption": {"text": "Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later.", "components": ["nocodb"], "sources": ["nocodb:sca40040b40cc", "nocodb:saf056bb4ff37"], "status": "REASONED"}, - "nocodb-jwt": {"text": "Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s536899ecd4a5"], "status": "REASONED"}, - "nocodb-cors": {"text": "Entry file allows every CORS origin.", "components": ["nocodb"], "sources": ["nocodb:sd74ca4df55b0"], "status": "REASONED"}, - "nocodb-proxy": {"text": "Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s635b35ce4689", "nocodb:sd74ca4df55b0"], "status": "REASONED"}, - "nocodb-packaging": {"text": "README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice).", "components": ["nocodb"], "sources": ["nocodb:s536899ecd4a5", "nocodb:sca0c9c0ecf24", "nocodb:sb4d5b659f7b8", "nocodb:s118384fa1d9f", "nocodb:s9bee4adc612e", "nocodb:s3f296c702bc5", "nocodb:s2769b9054778", "nocodb:s2b16e2c93606"], "status": "REASONED"}, - "baserow-bind": {"text": "Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s3f61036aca03"], "status": "REASONED"}, - "baserow-first": {"text": "First signup becomes staff; claim before exposure.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6"], "status": "REASONED"}, - "baserow-signup": {"text": "allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, - "baserow-secrets": {"text": "Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s1a472d828699"], "status": "REASONED"}, - "baserow-mfa": {"text": "Core registers a TOTP provider.", "components": ["baserow"], "sources": ["baserow:sd858f9ee45e0"], "status": "REASONED"}, - "appsmith-bind": {"text": "Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1"], "status": "REASONED"}, - "appsmith-publish": {"text": "Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4", "appsmith:sfa551c344e8b"], "status": "REASONED"}, - "appsmith-first": {"text": "First signup claims super-user while no users exist; claim before exposure.", "components": ["appsmith"], "sources": ["appsmith:sd1c55be91e30"], "status": "REASONED"}, - "appsmith-signup": {"text": "APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, - "appsmith-encryption": {"text": "Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, - "appsmith-rotation": {"text": "Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, - "budibase-publish": {"text": "Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12"], "status": "REASONED"}, - "budibase-couchdb": {"text": "Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s4d349b4d4ac3", "budibase:s2a033b3a4117", "budibase:scdcd8b6e7071", "budibase:sc74f36aa8340", "budibase:sf6e2a660380f", "budibase:sfdc76e71aff6", "budibase:s851ad716e105"], "status": "REASONED"}, - "budibase-secrets": {"text": "Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "budibase:s5209f2549c9b", "budibase:s32775c45ba51"], "status": "REASONED"}, - "budibase-sample-check": {"text": "This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s5209f2549c9b", "budibase:se993218560de", "budibase:s6179ec0d9abd", "budibase:s9cc27222e133", "budibase:sbe7e0469e7a8"], "status": "REASONED"}, - "budibase-init": {"text": "POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin.", "components": ["budibase"], "sources": ["budibase:s45379b35a980", "budibase:sc0a3ac85466f", "budibase:s4902d93986c7", "budibase:s2db467c4f1cf", "budibase:sbd761572fef0", "budibase:sb8f5a18ab4ee", "budibase:s7883460cabbf", "budibase:se18a9c7182fb"], "status": "REASONED"}, - "budibase-seed": {"text": "Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true.", "components": ["budibase"], "sources": ["budibase:sc0a3ac85466f", "budibase:sedd5e0682bf0", "budibase:s35ae42bd7a1c", "budibase:s816a78b83956"], "status": "REASONED"}, - "windmill-code": {"text": "Workers execute user scripts; a login enables code execution on worker hosts.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:sd775b7c95872"], "status": "REASONED"}, - "windmill-bind": {"text": "Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "REASONED"}, - "windmill-compose": {"text": "Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-sandbox": {"text": "DISABLE_NSJAIL defaults true; sandboxing is not enabled by default.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "REASONED"}, - "windmill-privileged": {"text": "Shipped worker uses privileged=true.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-smtp": {"text": "Port 25 serves email triggers; do not publish unless used.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-debugger": {"text": "Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "verify-nocodb-signup": {"text": "Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, - "verify-baserow-signup": {"text": "Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, - "verify-appsmith-signup": {"text": "Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, - "verify-external": {"text": "Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, - "windmill-default-login": {"text": "Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:s345ca635c837", "windmill:s03c33b490c08"], "status": "DEMONSTRATED", "evidence": "After the password was changed, `changeme` got `400` and the new password `200`.", "verify": [4]}, - "windmill-loopback": {"text": "SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "DEMONSTRATED", "evidence": "The server's only listener was on 127.0.0.1, at the test port the run set."}, - "windmill-sandbox-run": {"text": "Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "DEMONSTRATED", "evidence": "on the loopback run, without nsjail installed, the worker logged \"Nsjail sandboxing will NOT be available\"."}, - "verify-inventory": {"text": "Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation.", "components": ["docker", "baserow", "appsmith", "budibase", "windmill"], "sources": ["docker:s2fecb6db5480", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED", "verify": [1]}, - "verify-port-display": {"text": "IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered.", "components": ["docker", "docker-new", "docker-old"], "sources": ["docker:s2fecb6db5480", "docker-new:sca9a0e1e4b6a", "docker-old:sa202ad11d4ed"], "status": "REASONED", "verify": [1]}, - "verify-sample-tokens": {"text": "Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope.", "components": ["nocodb", "appsmith", "budibase", "windmill"], "sources": ["nocodb:s536899ecd4a5", "appsmith:s9fdcff5a28a4", "budibase:s735ead81ce9c", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On copies with the values replaced it printed \"no vendor sample tokens found\", and on an unreadable path it reported that it did not check.", "verify": [2]}, - "verify-nocodb-key": {"text": "docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified.", "components": ["nocodb", "docker"], "sources": ["nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, - "nocodb-key-limits": {"text": "Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key.", "components": ["nocodb", "docker"], "sources": ["nocodb:sca40040b40cc", "nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, - "nocodb-key-logic": {"text": "A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated.", "components": ["docker"], "sources": ["docker:sd34e76a09f6c"], "status": "DEMONSTRATED", "evidence": "With a stand-in for `docker exec`, the block printed \"non-empty\" for a set key, \"EMPTY or unset\" for an empty and an unset one, and \"not checked\" when the container was missing;"}, - "verify-windmill-control": {"text": "Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac"], "status": "REASONED"}, - "verify-couchdb": {"text": "Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control.", "components": ["budibase", "couchdb"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "couchdb:s34292991c4ec", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s2a033b3a4117", "budibase:s851ad716e105"], "status": "REASONED", "verify": [5]}, - "verify-litellm": {"text": "Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched.", "components": ["budibase", "litellm"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "litellm:sbe6fa0b81017", "budibase:s32775c45ba51"], "status": "REASONED", "verify": [5]}, - "verify-tcp-outcomes": {"text": "Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On loopback it printed \"connected\" for the control and an open port, \"timed out\" for a port whose accept queue was full (standing in for a filtered one) and \"refused\" for a closed port, and it stopped when the control was closed.", "verify": [6]}, - "verify-tcp-inputs": {"text": "Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "`::ffff:0:0` and `::ffff:7f00:1` passed and connected to a listener bound to 127.0.0.1", "verify": [6]} - } -} ---- # Low-code internal-tool builders: NocoDB, Baserow, Appsmith, Budibase, and Windmill - -**Version basis** +Written against NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0, Windmill v1.817.0, Docker CLI v27.2.0 and v27.1.2, Node.js v22.22.1, and CouchDB 3.5.2; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 51 claims: 7 demonstrated by a check that ran and 44 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-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. +These tools sit on top of your databases and APIs and let people build internal apps, forms and +automations quickly. That is also the exposure. Each one stores credentials for the systems it connects +to, and Windmill runs arbitrary code by design. Three first-run patterns recur. On a fresh instance, the +first visitor becomes the administrator. Open signup is on by default. And the vendor's own sample +configuration ships fixed secrets that too many deployments keep. Keep every one of them private +([docker.md](docker.md), [cloud-firewalls.md](cloud-firewalls.md), [tunnels.md](tunnels.md)), claim the +admin account before the instance is reachable, and front the web UI per +[fronting-auth.md](fronting-auth.md), [nginx.md](nginx.md) or [caddy.md](caddy.md). Treat the database +behind each tool as a store of every connected system's credentials ([secrets.md](secrets.md)). +Versions checked: NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0 and Windmill +v1.817.0. -Components (recorded basis): +## NocoDB -- nocodb: NocoDB (2026.09.0) -- docker: Docker docs (unknown) -- docker-new: Docker CLI (v27.2.0) -- docker-old: Docker CLI (v27.1.2) -- node: Node.js (v22.22.1) -- baserow: Baserow (2.3.4) -- appsmith: Appsmith (v2.4.1) -- budibase: Budibase (v3.46.0) -- couchdb: CouchDB (3.5.2) -- litellm: LiteLLM authentication source (6ef7b86748118ceecd95271727c2fa167ce55fec) -- windmill: Windmill (v1.817.0) +NocoDB listens on port 8080 (`process.env.PORT || '8080'`), and its entry point calls `listen` with no +host. For an omitted host, Node "will accept connections on the unspecified IPv6 address (`::`) when IPv6 +is available, or the unspecified IPv4 address (`0.0.0.0`) otherwise", so the server listens on every +interface; its code passes no host, so NocoDB offers no bind setting. Restrict it with the container +publication or a firewall. The first account to sign up gets the super-admin role, and later self-signups are allowed +unless invite-only signup is on; `invite_only_signup` defaults to `false`. Claim the admin account first, +or seed it with `NC_ADMIN_EMAIL` and `NC_ADMIN_PASSWORD`, then turn on invite-only signup. -Claims (full source mapping in the front matter): - -- private (REASONED; nocodb, baserow, appsmith, budibase, windmill): Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials. -- nocodb-bind (REASONED; nocodb, node): PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall. -- nocodb-first (REASONED; nocodb): First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD. -- nocodb-signup (REASONED; nocodb): invite_only_signup defaults false; enable to stop later self-signups. -- nocodb-encryption (REASONED; nocodb): Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later. -- nocodb-jwt (REASONED; nocodb): Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010. -- nocodb-cors (REASONED; nocodb): Entry file allows every CORS origin. -- nocodb-proxy (REASONED; nocodb): Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access. -- nocodb-packaging (REASONED; nocodb): README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice). -- baserow-bind (REASONED; baserow): Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW. -- baserow-first (REASONED; baserow): First signup becomes staff; claim before exposure. -- baserow-signup (REASONED; baserow): allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled. -- baserow-secrets (REASONED; baserow): Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own. -- baserow-mfa (REASONED; baserow): Core registers a TOTP provider. -- appsmith-bind (REASONED; appsmith): Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container. -- appsmith-publish (REASONED; appsmith): Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443. -- appsmith-first (REASONED; appsmith): First signup claims super-user while no users exist; claim before exposure. -- appsmith-signup (REASONED; appsmith): APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED. -- appsmith-encryption (REASONED; appsmith): Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start. -- appsmith-rotation (REASONED; appsmith): Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested. -- budibase-publish (REASONED; budibase): Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000. -- budibase-couchdb (REASONED; budibase): Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase. -- budibase-secrets (REASONED; budibase): Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established. -- budibase-sample-check (REASONED; budibase): This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran. -- budibase-init (REASONED; budibase): POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin. -- budibase-seed (REASONED; budibase): Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true. -- windmill-code (REASONED; windmill): Workers execute user scripts; a login enables code execution on worker hosts. -- windmill-bind (REASONED; windmill): Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed. -- windmill-compose (REASONED; windmill): Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred. -- windmill-sandbox (REASONED; windmill): DISABLE_NSJAIL defaults true; sandboxing is not enabled by default. -- windmill-privileged (REASONED; windmill): Shipped worker uses privileged=true. -- windmill-smtp (REASONED; windmill): Port 25 serves email triggers; do not publish unless used. -- windmill-debugger (REASONED; windmill): Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments. -- verify-nocodb-signup (REASONED; nocodb): Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login. -- verify-baserow-signup (REASONED; baserow): Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login. -- verify-appsmith-signup (REASONED; appsmith): Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts. -- verify-external (REASONED; baserow, budibase, windmill): Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation. -- windmill-default-login (DEMONSTRATED; windmill): Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200. -- windmill-loopback (DEMONSTRATED; windmill): SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port. -- windmill-sandbox-run (DEMONSTRATED; windmill): Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation. -- verify-inventory (REASONED; docker, baserow, appsmith, budibase, windmill): Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation. -- verify-port-display (REASONED; docker, docker-new, docker-old): IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered. -- verify-sample-tokens (DEMONSTRATED; nocodb, appsmith, budibase, windmill): Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope. -- verify-nocodb-key (REASONED; nocodb, docker): docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified. -- nocodb-key-limits (REASONED; nocodb, docker): Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key. -- nocodb-key-logic (DEMONSTRATED; docker): A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated. -- verify-windmill-control (REASONED; windmill): Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed. -- verify-couchdb (REASONED; budibase, couchdb): Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control. -- verify-litellm (REASONED; budibase, litellm): Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched. -- verify-tcp-outcomes (DEMONSTRATED; baserow, budibase, windmill): Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated. -- verify-tcp-inputs (DEMONSTRATED; baserow, budibase, windmill): Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control. - - -These tools sit on top of your databases and APIs and let people build internal apps, forms and -automations quickly. That is also the exposure. Each one stores credentials for the systems it connects -to, and Windmill runs arbitrary code by design. Three first-run patterns recur. On a fresh instance, the -first visitor becomes the administrator. Open signup is on by default. And the vendor's own sample -configuration ships fixed secrets that too many deployments keep. Keep every one of them private -([docker.md](docker.md), [cloud-firewalls.md](cloud-firewalls.md), [tunnels.md](tunnels.md)), claim the -admin account before the instance is reachable, and front the web UI per -[fronting-auth.md](fronting-auth.md), [nginx.md](nginx.md) or [caddy.md](caddy.md). Treat the database -behind each tool as a store of every connected system's credentials ([secrets.md](secrets.md)). -Versions checked: NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0 and Windmill -v1.817.0. - -## NocoDB - -NocoDB listens on port 8080 (`process.env.PORT || '8080'`), and its entry point calls `listen` with no -host. For an omitted host, Node "will accept connections on the unspecified IPv6 address (`::`) when IPv6 -is available, or the unspecified IPv4 address (`0.0.0.0`) otherwise", so the server listens on every -interface; its code passes no host, so NocoDB offers no bind setting. Restrict it with the container -publication or a firewall. The first account to sign up gets the super-admin role, and later self-signups are allowed -unless invite-only signup is on; `invite_only_signup` defaults to `false`. Claim the admin account first, -or seed it with `NC_ADMIN_EMAIL` and `NC_ADMIN_PASSWORD`, then turn on invite-only signup. - -Two secrets need your attention: +Two secrets need your attention: - **`NC_CONNECTION_ENCRYPT_KEY`.** When it is unset, the encryption helper returns the value unchanged, so the credentials of every external database you connect are stored in plaintext in NocoDB's meta @@ -735,3 +462,302 @@ mean to publish is the finding. - Windmill v1.817.0 worker (`DISABLE_NSJAIL`): https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs - Windmill v1.817.0 Compose file: https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml - Windmill v1.817.0 seeded-user migrations: https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql and https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql + + + +## 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/low-code-builders.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): + +- nocodb: NocoDB (2026.09.0) +- baserow: Baserow (2.3.4) +- appsmith: Appsmith (v2.4.1) +- budibase: Budibase (v3.46.0) +- windmill: Windmill (v1.817.0) +- docker: Docker docs (unknown) +- docker-new: Docker CLI (v27.2.0) +- docker-old: Docker CLI (v27.1.2) +- node: Node.js (v22.22.1) +- couchdb: CouchDB (3.5.2) +- litellm: LiteLLM authentication source (6ef7b86748118ceecd95271727c2fa167ce55fec) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; nocodb, baserow, appsmith, budibase, windmill): Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials. +- nocodb-bind (REASONED; nocodb, node): PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall. +- nocodb-first (REASONED; nocodb): First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD. +- nocodb-signup (REASONED; nocodb): invite_only_signup defaults false; enable to stop later self-signups. +- nocodb-encryption (REASONED; nocodb): Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later. +- nocodb-jwt (REASONED; nocodb): Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010. +- nocodb-cors (REASONED; nocodb): Entry file allows every CORS origin. +- nocodb-proxy (REASONED; nocodb): Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access. +- nocodb-packaging (REASONED; nocodb): README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice). +- baserow-bind (REASONED; baserow): Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW. +- baserow-first (REASONED; baserow): First signup becomes staff; claim before exposure. +- baserow-signup (REASONED; baserow): allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled. +- baserow-secrets (REASONED; baserow): Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own. +- baserow-mfa (REASONED; baserow): Core registers a TOTP provider. +- appsmith-bind (REASONED; appsmith): Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container. +- appsmith-publish (REASONED; appsmith): Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443. +- appsmith-first (REASONED; appsmith): First signup claims super-user while no users exist; claim before exposure. +- appsmith-signup (REASONED; appsmith): APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED. +- appsmith-encryption (REASONED; appsmith): Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start. +- appsmith-rotation (REASONED; appsmith): Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested. +- budibase-publish (REASONED; budibase): Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000. +- budibase-couchdb (REASONED; budibase): Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase. +- budibase-secrets (REASONED; budibase): Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established. +- budibase-sample-check (REASONED; budibase): This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran. +- budibase-init (REASONED; budibase): POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin. +- budibase-seed (REASONED; budibase): Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true. +- windmill-code (REASONED; windmill): Workers execute user scripts; a login enables code execution on worker hosts. +- windmill-bind (REASONED; windmill): Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed. +- windmill-compose (REASONED; windmill): Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred. +- windmill-sandbox (REASONED; windmill): DISABLE_NSJAIL defaults true; sandboxing is not enabled by default. +- windmill-privileged (REASONED; windmill): Shipped worker uses privileged=true. +- windmill-smtp (REASONED; windmill): Port 25 serves email triggers; do not publish unless used. +- windmill-debugger (REASONED; windmill): Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments. +- verify-nocodb-signup (REASONED; nocodb): Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login. +- verify-baserow-signup (REASONED; baserow): Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login. +- verify-appsmith-signup (REASONED; appsmith): Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts. +- verify-external (REASONED; baserow, budibase, windmill): Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation. +- windmill-default-login (DEMONSTRATED; windmill): Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200. +- windmill-loopback (DEMONSTRATED; windmill): SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port. +- windmill-sandbox-run (DEMONSTRATED; windmill): Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation. +- verify-inventory (REASONED; docker, baserow, appsmith, budibase, windmill): Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation. +- verify-port-display (REASONED; docker, docker-new, docker-old): IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered. +- verify-sample-tokens (DEMONSTRATED; nocodb, appsmith, budibase, windmill): Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope. +- verify-nocodb-key (REASONED; nocodb, docker): docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified. +- nocodb-key-limits (REASONED; nocodb, docker): Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key. +- nocodb-key-logic (DEMONSTRATED; docker): A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated. +- verify-windmill-control (REASONED; windmill): Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed. +- verify-couchdb (REASONED; budibase, couchdb): Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control. +- verify-litellm (REASONED; budibase, litellm): Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched. +- verify-tcp-outcomes (DEMONSTRATED; baserow, budibase, windmill): Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated. +- verify-tcp-inputs (DEMONSTRATED; baserow, budibase, windmill): Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "43cf22968b497a23408d386c38d01f763e391f115cf9a92f4fb443ce28cea9a8", + "components": { + "nocodb": { + "name": "NocoDB", + "basis": "2026.09.0", + "sources": { + "sa19c6557b712": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/Noco.ts", + "s635b35ce4689": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/trustProxy.ts", + "sd74ca4df55b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/run/dockerEntry.ts", + "s5ea55dc5f0d9": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/services/users/users.service.ts", + "sd0e9f451673f": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/interface/AppSettings.ts", + "s8a1de0bc97b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initAdminFromEnv.ts", + "sca40040b40cc": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/encryptDecrypt.ts", + "saf056bb4ff37": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initDataSourceEncryption.ts", + "sbfbf630008e1": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start.sh", + "se5da9707c684": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start-litestream.sh", + "s536899ecd4a5": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md", + "sca0c9c0ecf24": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L56-L75", + "sb4d5b659f7b8": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L77-L90", + "s2769b9054778": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L82", + "s2b16e2c93606": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L90", + "s118384fa1d9f": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L95", + "s9bee4adc612e": "https://api.github.com/repos/nocodb/nocodb/releases/tags/2026.09.0", + "s3f296c702bc5": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/package.json#L3", + "se81c30b3f43b": "https://github.com/nocodb/nocodb/blob/2026.09.0/docker-compose/examples/external-postgres-and-redis/docker.env" + } + }, + "baserow": { + "name": "Baserow", + "basis": "2.3.4", + "sources": { + "s1b31314982f4": "https://github.com/baserow/baserow/blob/2.3.4/docker-compose.yml", + "s3f61036aca03": "https://github.com/baserow/baserow/blob/2.3.4/docs/installation/install-with-docker.md", + "s34b87f60dcc6": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/user/handler.py", + "s4588448964a2": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/models.py", + "sd858f9ee45e0": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/apps.py", + "s1a472d828699": "https://github.com/baserow/baserow/blob/2.3.4/.env.example" + } + }, + "appsmith": { + "name": "Appsmith", + "basis": "v2.4.1", + "sources": { + "s2650629933a1": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/resources/application-ce.properties", + "sd1c55be91e30": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java", + "se3cd894dc579": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java", + "sa68daf87cde7": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/fs/opt/appsmith/entrypoint.sh", + "s9fdcff5a28a4": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/docker-compose.yml", + "sfa551c344e8b": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/aws_ami/docker-compose.yml" + } + }, + "budibase": { + "name": "Budibase", + "basis": "v3.46.0", + "sources": { + "s735ead81ce9c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env", + "s74d858053d12": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml", + "s7fbf706a4c8f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf", + "s45379b35a980": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts", + "sc0a3ac85466f": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts", + "s4902d93986c7": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts", + "s5209f2549c9b": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env#L4-L16", + "s9afbeb2063b5": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L111-L120", + "s32775c45ba51": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L129-L144", + "s4d349b4d4ac3": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L86", + "s2a033b3a4117": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L236-L239", + "s2db467c4f1cf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L40-L43", + "sbd761572fef0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L133-L167", + "sb8f5a18ab4ee": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L13-L28", + "s7883460cabbf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L75-L79", + "se18a9c7182fb": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts#L229-L274", + "s816a78b83956": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/server/src/startup/index.ts#L226-L266", + "sedd5e0682bf0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/src/environment.ts#L267-L268", + "s35ae42bd7a1c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L27-L28", + "scdcd8b6e7071": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L89-L91", + "sc74f36aa8340": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L151-L154", + "sf6e2a660380f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/VERSION#L1", + "sfdc76e71aff6": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/runner.sh#L165-L166", + "s851ad716e105": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/docker-entrypoint.sh#L70-L75", + "se993218560de": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/hosting.properties#L4-L18", + "s6179ec0d9abd": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot#L11-L15", + "s9cc27222e133": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/tests/jestEnv.ts#L9-L10", + "sbe7e0469e7a8": "https://github.com/Budibase/budibase/blob/v3.46.0/scripts/dev/manage.js#L11-L12" + } + }, + "windmill": { + "name": "Windmill", + "basis": "v1.817.0", + "sources": { + "s91439e21d4ac": "https://github.com/windmill-labs/windmill/blob/v1.817.0/README.md", + "s8e4bbba271f2": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/src/main.rs", + "sd775b7c95872": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs", + "sfcc72cf06c9e": "https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml", + "s345ca635c837": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql", + "s03c33b490c08": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql" + } + }, + "docker": { + "name": "Docker docs", + "basis": "unknown", + "sources": { + "sd34e76a09f6c": "https://docs.docker.com/reference/cli/docker/container/exec/", + "s2fecb6db5480": "https://docs.docker.com/engine/network/firewall-iptables/" + } + }, + "docker-new": { + "name": "Docker CLI", + "basis": "v27.2.0", + "sources": { + "sca9a0e1e4b6a": "https://github.com/docker/cli/blob/v27.2.0/cli/command/formatter/container.go" + } + }, + "docker-old": { + "name": "Docker CLI", + "basis": "v27.1.2", + "sources": { + "sa202ad11d4ed": "https://github.com/docker/cli/blob/v27.1.2/cli/command/formatter/container.go" + } + }, + "node": { + "name": "Node.js", + "basis": "v22.22.1", + "sources": { + "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" + } + }, + "couchdb": { + "name": "CouchDB", + "basis": "3.5.2", + "sources": { + "s34292991c4ec": "https://github.com/apache/couchdb/blob/3.5.2/src/docs/src/api/server/authn.rst" + } + }, + "litellm": { + "name": "LiteLLM authentication source", + "basis": "6ef7b86748118ceecd95271727c2fa167ce55fec", + "sources": { + "sbe6fa0b81017": "https://raw.githubusercontent.com/BerriAI/litellm/6ef7b86748118ceecd95271727c2fa167ce55fec/litellm/proxy/auth/user_api_key_auth.py" + } + } + }, + "claims": { + "private": {"text": "Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials.", "components": ["nocodb", "baserow", "appsmith", "budibase", "windmill"], "sources": ["nocodb:sd74ca4df55b0", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "nocodb-bind": {"text": "PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall.", "components": ["nocodb", "node"], "sources": ["nocodb:sa19c6557b712", "nocodb:sd74ca4df55b0", "node:sfb3e306c102d"], "status": "REASONED"}, + "nocodb-first": {"text": "First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:s8a1de0bc97b0"], "status": "REASONED"}, + "nocodb-signup": {"text": "invite_only_signup defaults false; enable to stop later self-signups.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, + "nocodb-encryption": {"text": "Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later.", "components": ["nocodb"], "sources": ["nocodb:sca40040b40cc", "nocodb:saf056bb4ff37"], "status": "REASONED"}, + "nocodb-jwt": {"text": "Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s536899ecd4a5"], "status": "REASONED"}, + "nocodb-cors": {"text": "Entry file allows every CORS origin.", "components": ["nocodb"], "sources": ["nocodb:sd74ca4df55b0"], "status": "REASONED"}, + "nocodb-proxy": {"text": "Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s635b35ce4689", "nocodb:sd74ca4df55b0"], "status": "REASONED"}, + "nocodb-packaging": {"text": "README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice).", "components": ["nocodb"], "sources": ["nocodb:s536899ecd4a5", "nocodb:sca0c9c0ecf24", "nocodb:sb4d5b659f7b8", "nocodb:s118384fa1d9f", "nocodb:s9bee4adc612e", "nocodb:s3f296c702bc5", "nocodb:s2769b9054778", "nocodb:s2b16e2c93606"], "status": "REASONED"}, + "baserow-bind": {"text": "Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s3f61036aca03"], "status": "REASONED"}, + "baserow-first": {"text": "First signup becomes staff; claim before exposure.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6"], "status": "REASONED"}, + "baserow-signup": {"text": "allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, + "baserow-secrets": {"text": "Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s1a472d828699"], "status": "REASONED"}, + "baserow-mfa": {"text": "Core registers a TOTP provider.", "components": ["baserow"], "sources": ["baserow:sd858f9ee45e0"], "status": "REASONED"}, + "appsmith-bind": {"text": "Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1"], "status": "REASONED"}, + "appsmith-publish": {"text": "Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4", "appsmith:sfa551c344e8b"], "status": "REASONED"}, + "appsmith-first": {"text": "First signup claims super-user while no users exist; claim before exposure.", "components": ["appsmith"], "sources": ["appsmith:sd1c55be91e30"], "status": "REASONED"}, + "appsmith-signup": {"text": "APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, + "appsmith-encryption": {"text": "Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, + "appsmith-rotation": {"text": "Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, + "budibase-publish": {"text": "Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12"], "status": "REASONED"}, + "budibase-couchdb": {"text": "Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s4d349b4d4ac3", "budibase:s2a033b3a4117", "budibase:scdcd8b6e7071", "budibase:sc74f36aa8340", "budibase:sf6e2a660380f", "budibase:sfdc76e71aff6", "budibase:s851ad716e105"], "status": "REASONED"}, + "budibase-secrets": {"text": "Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "budibase:s5209f2549c9b", "budibase:s32775c45ba51"], "status": "REASONED"}, + "budibase-sample-check": {"text": "This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s5209f2549c9b", "budibase:se993218560de", "budibase:s6179ec0d9abd", "budibase:s9cc27222e133", "budibase:sbe7e0469e7a8"], "status": "REASONED"}, + "budibase-init": {"text": "POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin.", "components": ["budibase"], "sources": ["budibase:s45379b35a980", "budibase:sc0a3ac85466f", "budibase:s4902d93986c7", "budibase:s2db467c4f1cf", "budibase:sbd761572fef0", "budibase:sb8f5a18ab4ee", "budibase:s7883460cabbf", "budibase:se18a9c7182fb"], "status": "REASONED"}, + "budibase-seed": {"text": "Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true.", "components": ["budibase"], "sources": ["budibase:sc0a3ac85466f", "budibase:sedd5e0682bf0", "budibase:s35ae42bd7a1c", "budibase:s816a78b83956"], "status": "REASONED"}, + "windmill-code": {"text": "Workers execute user scripts; a login enables code execution on worker hosts.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:sd775b7c95872"], "status": "REASONED"}, + "windmill-bind": {"text": "Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "REASONED"}, + "windmill-compose": {"text": "Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-sandbox": {"text": "DISABLE_NSJAIL defaults true; sandboxing is not enabled by default.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "REASONED"}, + "windmill-privileged": {"text": "Shipped worker uses privileged=true.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-smtp": {"text": "Port 25 serves email triggers; do not publish unless used.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-debugger": {"text": "Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "verify-nocodb-signup": {"text": "Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, + "verify-baserow-signup": {"text": "Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, + "verify-appsmith-signup": {"text": "Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, + "verify-external": {"text": "Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-default-login": {"text": "Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:s345ca635c837", "windmill:s03c33b490c08"], "status": "DEMONSTRATED", "evidence": "After the password was changed, `changeme` got `400` and the new password `200`.", "verify": [4]}, + "windmill-loopback": {"text": "SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "DEMONSTRATED", "evidence": "The server's only listener was on 127.0.0.1, at the test port the run set."}, + "windmill-sandbox-run": {"text": "Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "DEMONSTRATED", "evidence": "on the loopback run, without nsjail installed, the worker logged \"Nsjail sandboxing will NOT be available\"."}, + "verify-inventory": {"text": "Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation.", "components": ["docker", "baserow", "appsmith", "budibase", "windmill"], "sources": ["docker:s2fecb6db5480", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED", "verify": [1]}, + "verify-port-display": {"text": "IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered.", "components": ["docker", "docker-new", "docker-old"], "sources": ["docker:s2fecb6db5480", "docker-new:sca9a0e1e4b6a", "docker-old:sa202ad11d4ed"], "status": "REASONED", "verify": [1]}, + "verify-sample-tokens": {"text": "Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope.", "components": ["nocodb", "appsmith", "budibase", "windmill"], "sources": ["nocodb:s536899ecd4a5", "appsmith:s9fdcff5a28a4", "budibase:s735ead81ce9c", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On copies with the values replaced it printed \"no vendor sample tokens found\", and on an unreadable path it reported that it did not check.", "verify": [2]}, + "verify-nocodb-key": {"text": "docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified.", "components": ["nocodb", "docker"], "sources": ["nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, + "nocodb-key-limits": {"text": "Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key.", "components": ["nocodb", "docker"], "sources": ["nocodb:sca40040b40cc", "nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, + "nocodb-key-logic": {"text": "A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated.", "components": ["docker"], "sources": ["docker:sd34e76a09f6c"], "status": "DEMONSTRATED", "evidence": "With a stand-in for `docker exec`, the block printed \"non-empty\" for a set key, \"EMPTY or unset\" for an empty and an unset one, and \"not checked\" when the container was missing;"}, + "verify-windmill-control": {"text": "Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac"], "status": "REASONED"}, + "verify-couchdb": {"text": "Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control.", "components": ["budibase", "couchdb"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "couchdb:s34292991c4ec", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s2a033b3a4117", "budibase:s851ad716e105"], "status": "REASONED", "verify": [5]}, + "verify-litellm": {"text": "Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched.", "components": ["budibase", "litellm"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "litellm:sbe6fa0b81017", "budibase:s32775c45ba51"], "status": "REASONED", "verify": [5]}, + "verify-tcp-outcomes": {"text": "Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On loopback it printed \"connected\" for the control and an open port, \"timed out\" for a port whose accept queue was full (standing in for a filtered one) and \"refused\" for a closed port, and it stopped when the control was closed.", "verify": [6]}, + "verify-tcp-inputs": {"text": "Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "`::ffff:0:0` and `::ffff:7f00:1` passed and connected to a listener bound to 127.0.0.1", "verify": [6]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/mfa.md b/plugin/skills/secure-deployment-config/references/mfa.md index a45f3bce0..fb2c52b2c 100644 --- a/plugin/skills/secure-deployment-config/references/mfa.md +++ b/plugin/skills/secure-deployment-config/references/mfa.md @@ -1,6 +1,6 @@ # Multi-factor authentication (MFA) -Written against TOTP standard (no recorded version), Caddy forward_auth minimum 2.5.1, google-authenticator-libpam source 1.11, MySQL 8.0 multifactor authentication 8.0, MySQL 8.4 multifactor authentication 8.4, MySQL 8.4 WebAuthn authentication 8.4, and authentik source version/2026.8.3, plus 10 more recorded bases; 14 further sources are pinned to a commit and 13 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 35 claims: 2 demonstrated by a check that ran and 33 reasoned from cited sources. +Written against TOTP standard (no recorded version), authentik source version/2026.8.3, Keycloak documentation 26.7.4, oauth2-proxy documentation v7.15.4, Caddy forward_auth minimum 2.5.1, google-authenticator-libpam source 1.11, and MySQL 8.0 multifactor authentication 8.0, plus 10 more recorded bases; 14 further sources are pinned to a commit and 13 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 35 claims: 2 demonstrated by a check that ran and 33 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. @@ -171,6 +171,9 @@ Components (recorded basis): - uri: Authenticator key URI format (unknown) - webauthn: WebAuthn standard (unknown) - authelia: Authelia proxy support (unknown) +- authentik-source: authentik source (version/2026.8.3) +- keycloak-source: Keycloak documentation (26.7.4) +- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) - caddy-min: Caddy forward_auth minimum (2.5.1) - duo: Duo editions (unknown) - duo-push-policy: Duo Verified Push policy (unknown) @@ -184,9 +187,6 @@ Components (recorded basis): - mysql: MySQL 8.0 multifactor authentication (8.0) - mysql-8-4: MySQL 8.4 multifactor authentication (8.4) - mysql-webauthn: MySQL 8.4 WebAuthn authentication (8.4) -- authentik-source: authentik source (version/2026.8.3) -- keycloak-source: Keycloak documentation (26.7.4) -- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) - entra-docs: Microsoft Entra documentation (231747abc59ae3d50c74e215c1cdd592eb980723) - owasp-docs: OWASP cheat sheets (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) - otp-source: pquerna/otp source (617f8d5f3f518bae0f5c3ec14a17ca19f2a6f870) @@ -291,6 +291,28 @@ Claims (full source mapping in the JSON block below): "s1409ac8fe3d9": "https://www.authelia.com/integration/proxies/support/" } }, + "authentik-source": { + "name": "authentik source", + "basis": "version/2026.8.3", + "sources": { + "s18275a7072e9": "https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/stages/authenticator_validate/stage.py#L80-L108" + } + }, + "keycloak-source": { + "name": "Keycloak documentation", + "basis": "26.7.4", + "sources": { + "s1e55f7daaa0d": "https://github.com/keycloak/keycloak/blob/26.7.4/docs/documentation/server_admin/topics/authentication/otp-policies.adoc#L2-L21" + } + }, + "oauth2-proxy-source": { + "name": "oauth2-proxy documentation", + "basis": "v7.15.4", + "sources": { + "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", + "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" + } + }, "caddy-min": { "name": "Caddy forward_auth minimum", "basis": "2.5.1", @@ -385,28 +407,6 @@ Claims (full source mapping in the JSON block below): "s807a77fa8872": "https://dev.mysql.com/doc/refman/8.4/en/webauthn-pluggable-authentication.html" } }, - "authentik-source": { - "name": "authentik source", - "basis": "version/2026.8.3", - "sources": { - "s18275a7072e9": "https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/stages/authenticator_validate/stage.py#L80-L108" - } - }, - "keycloak-source": { - "name": "Keycloak documentation", - "basis": "26.7.4", - "sources": { - "s1e55f7daaa0d": "https://github.com/keycloak/keycloak/blob/26.7.4/docs/documentation/server_admin/topics/authentication/otp-policies.adoc#L2-L21" - } - }, - "oauth2-proxy-source": { - "name": "oauth2-proxy documentation", - "basis": "v7.15.4", - "sources": { - "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", - "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" - } - }, "entra-docs": { "name": "Microsoft Entra documentation", "basis": "231747abc59ae3d50c74e215c1cdd592eb980723", diff --git a/plugin/skills/secure-deployment-config/references/mlflow.md b/plugin/skills/secure-deployment-config/references/mlflow.md index e394238f2..5d125755f 100644 --- a/plugin/skills/secure-deployment-config/references/mlflow.md +++ b/plugin/skills/secure-deployment-config/references/mlflow.md @@ -1,204 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-05", - "documentation_checked": "2026-09", - "body_sha256": "ee30ff570fece0f829e8b7a87b1a777387f0b1f32d2e160049713119d344088e", - "components": { - "docs": { - "name": "MLflow documentation", - "basis": "unknown", - "sources": { - "scbbc07258943": "https://mlflow.org/docs/latest/self-hosting/security/basic-http-auth/", - "sc3a2d70cabf9": "https://mlflow.org/docs/latest/api_reference/auth/rest-api.html", - "s5b79d1871e0b": "https://mlflow.org/docs/latest/api_reference/cli.html", - "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server", - "s996e587e947a": "https://mlflow.org/docs/latest/api_reference/rest-api.html", - "s559e7a8271b9": "https://mlflow.org/docs/latest/api_reference/cli.html#mlflow" - } - }, - "source": { - "name": "MLflow pinned source", - "basis": "v3.16.1", - "sources": { - "sf7285d252e29": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/cli_args.py#L162-L180", - "sa3993a9b625f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L369-L541", - "sd775f10c4d07": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/auth/__init__.py#L6008-L6021", - "s349a81b9478e": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/__init__.py#L401-L402", - "s99d30948902f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L85-L86", - "s687f1c9977cd": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L94-L105", - "s1661739df39d": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/environment_variables.py#L493-L503", - "s5801c6ac1ac2": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/rest_utils.py#L254-L292" - } - }, - "middleware-min": { - "name": "MLflow security middleware minimum", - "basis": "3.5.0", - "sources": { - "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server" - } - }, - "docker": { - "name": "Docker Engine loopback publication minimum", - "basis": "28.0.0", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - }, - "curl": { - "name": "curl minimum write-out version", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "nginx": { - "name": "nginx proxy implementation", - "basis": "release-1.28.0", - "sources": { - "s6dfa81e36717": "https://github.com/nginx/nginx/blob/release-1.28.0/src/http/modules/ngx_http_proxy_module.c#L305-L310" - } - }, - "caddy": { - "name": "Caddy reverse proxy parser", - "basis": "v2.11.4", - "sources": { - "s52039756d4f4": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L40", - "s9dde4a2bda4c": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L58-L62" - } - }, - "iproute2": { - "name": "iproute2 ss manual", - "basis": "v6.12.0", - "sources": { - "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", - "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", - "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", - "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375" - } - }, - "cf-docs": { - "name": "Cloudflare 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", - "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", - "sbec4996a7089": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33", - "s543b1f2eb29c": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49", - "sc565f2d80e6e": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59" - } - }, - "oauth2-proxy-source": { - "name": "oauth2-proxy documentation", - "basis": "v7.15.4", - "sources": { - "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", - "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" - } - }, - "authelia-source": { - "name": "Authelia source", - "basis": "v4.39.28", - "sources": { - "s768b165b44a0": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507", - "sd4dd76e70475": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537" - } - } - }, - "claims": { - "default-host": {"text": "v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, - "default-port": {"text": "v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, - "default-auth": {"text": "Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s2517159af609"], "status": "REASONED"}, - "container-bind": {"text": "Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports.", "components": ["docker", "docs"], "sources": ["docker:s1e53417c513d", "docs:s5b79d1871e0b"], "status": "REASONED"}, - "middleware-scope": {"text": "Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it.", "components": ["middleware-min", "docs"], "sources": ["middleware-min:s2517159af609", "docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "host-allowlist": {"text": "Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "cors": {"text": "Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "middleware-disable": {"text": "Do not use --disable-security-middleware outside a test.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "tls": {"text": "There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname.", "components": ["docs", "nginx", "caddy"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609", "nginx:s6dfa81e36717", "caddy:s52039756d4f4", "caddy:s9dde4a2bda4c"], "status": "REASONED"}, - "client-tls": {"text": "Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, - "auth-app": {"text": "Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "csrf-key": {"text": "Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "env-file": {"text": "Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching.", "components": ["docs", "source"], "sources": ["docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, - "key-exposure": {"text": "Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure.", "components": ["source", "docs"], "sources": ["source:sd775f10c4d07", "source:s349a81b9478e", "docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, - "admin-bootstrap": {"text": "No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "default-permission": {"text": "default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "auth-database": {"text": "database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "auth-config": {"text": "MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "password-rotation": {"text": "PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping.", "components": ["docs", "curl"], "sources": ["docs:sc3a2d70cabf9", "curl:s2b2686afaf41"], "status": "REASONED"}, - "user-creation": {"text": "User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:sc3a2d70cabf9"], "status": "REASONED"}, - "client-credentials": {"text": "~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "rate-limit": {"text": "The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, - "mfa": {"text": "MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination.", "components": ["docs", "cf-docs", "oauth2-proxy-source", "authelia-source"], "sources": ["docs:scbbc07258943", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "oauth2-proxy-source:sa403e3d826b4", "oauth2-proxy-source:se7a200583def", "authelia-source:s768b165b44a0", "authelia-source:sd4dd76e70475"], "status": "REASONED"}, - "proxy-token": {"text": "MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together.", "components": ["docs", "source", "cf-docs"], "sources": ["docs:s2517159af609", "source:s1661739df39d", "source:s5801c6ac1ac2", "cf-docs:sbec4996a7089", "cf-docs:s543b1f2eb29c", "cf-docs:sc565f2d80e6e"], "status": "REASONED"}, - "artifact-proxy": {"text": "--serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, - "artifact-existing": {"text": "Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, - "artifact-direct": {"text": "With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, - "verify-bind": {"text": "Read every listener and require 5000 on 127.0.0.1.", "components": ["source", "iproute2"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850"], "status": "REASONED", "verify": [1]}, - "verify-network": {"text": "Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive.", "components": ["curl", "docs"], "sources": ["curl:s2b2686afaf41", "docs:s5b79d1871e0b"], "status": "REASONED", "verify": [1]}, - "verify-public-auth": {"text": "POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass.", "components": ["docs", "curl"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-backend-auth": {"text": "Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s5b79d1871e0b", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]}, - "verify-authorization": {"text": "Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]} - } -} ---- # MLflow tracking server: no authentication by default - -**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): - -- docs: MLflow documentation (unknown) -- source: MLflow pinned source (v3.16.1) -- middleware-min: MLflow security middleware minimum (3.5.0) -- docker: Docker Engine loopback publication minimum (28.0.0) -- curl: curl minimum write-out version (7.75.0) -- nginx: nginx proxy implementation (release-1.28.0) -- caddy: Caddy reverse proxy parser (v2.11.4) -- iproute2: iproute2 ss manual (v6.12.0) -- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) -- authelia-source: Authelia source (v4.39.28) +Written against MLflow pinned source v3.16.1, MLflow security middleware minimum 3.5.0, Docker Engine loopback publication minimum 28.0.0, nginx proxy implementation release-1.28.0, Caddy reverse proxy parser v2.11.4, oauth2-proxy documentation v7.15.4, and Authelia source v4.39.28, plus 2 more recorded bases; 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 32 claims, all reasoned from cited sources. -Claims (full source mapping in the front matter): - -- default-host (REASONED; source): v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting. -- default-port (REASONED; source): v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it. -- default-auth (REASONED; docs): Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts. -- container-bind (REASONED; docker, docs): Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports. -- middleware-scope (REASONED; middleware-min, docs): Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it. -- host-allowlist (REASONED; docs): Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules. -- cors (REASONED; docs): Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin. -- middleware-disable (REASONED; docs): Do not use --disable-security-middleware outside a test. -- tls (REASONED; docs, nginx, caddy): There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname. -- client-tls (REASONED; docs): Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production. -- auth-app (REASONED; docs): Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label. -- csrf-key (REASONED; docs): Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control. -- env-file (REASONED; docs, source): Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching. -- key-exposure (REASONED; source, docs): Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure. -- admin-bootstrap (REASONED; docs): No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply. -- default-permission (REASONED; docs): default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant. -- auth-database (REASONED; docs): database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example. -- auth-config (REASONED; docs): MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic. -- password-rotation (REASONED; docs, curl): PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping. -- user-creation (REASONED; docs): User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user. -- client-credentials (REASONED; docs): ~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD. -- rate-limit (REASONED; docs): The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS. -- mfa (REASONED; docs, cf-docs, oauth2-proxy-source, authelia-source): MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination. -- proxy-token (REASONED; docs, source, cf-docs): MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together. -- artifact-proxy (REASONED; docs): --serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none. -- artifact-existing (REASONED; docs): Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly. -- artifact-direct (REASONED; docs): With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets. -- verify-bind (REASONED; source, iproute2): Read every listener and require 5000 on 127.0.0.1. -- verify-network (REASONED; curl, docs): Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive. -- verify-public-auth (REASONED; docs, curl): POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass. -- verify-backend-auth (REASONED; docs): Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement. -- verify-authorization (REASONED; docs): Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is 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. `mlflow server` serves the tracking UI and REST API at `http://127.0.0.1:5000` (as of v3.16.1, unless `MLFLOW_HOST` or `MLFLOW_PORT` is set) and performs no authentication: anyone who can reach the port can read, alter, and delete experiments, runs, registered models, and (with artifact proxying on) the artifacts themselves. Authentication is opt-in through a separate app, and the server has no dedicated TLS flags (though `mlflow server --uvicorn-opts` can forward `--ssl-keyfile`/`--ssl-certfile` to the default Uvicorn server); MLflow's tracking-server documentation recommends a reverse proxy or VPN for both. @@ -394,3 +198,225 @@ REASONED: preceding block; listener inventory, external TCP reachability, proxy/ - Cloudflare service-token Service Auth requirement, request headers and configurable single-header alternative (pinned commit c896795b8e8bab68d05ae78d64c9cbcda1fdb104, checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33, https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49, https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59 - oauth2-proxy v7.15.4 provider and upstream configuration; these entries do not establish MFA enforcement (checked October 2026): https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108, https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274 - Authelia v4.39.28 first matching rule, default policy and two_factor requirement (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507, https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537 + + + +## 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/mlflow.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): + +- source: MLflow pinned source (v3.16.1) +- middleware-min: MLflow security middleware minimum (3.5.0) +- docs: MLflow documentation (unknown) +- docker: Docker Engine loopback publication minimum (28.0.0) +- nginx: nginx proxy implementation (release-1.28.0) +- caddy: Caddy reverse proxy parser (v2.11.4) +- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) +- authelia-source: Authelia source (v4.39.28) +- curl: curl minimum write-out version (7.75.0) +- iproute2: iproute2 ss manual (v6.12.0) + +Claims (full source mapping in the JSON block below): + +- default-host (REASONED; source): v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting. +- default-port (REASONED; source): v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it. +- default-auth (REASONED; docs): Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts. +- container-bind (REASONED; docker, docs): Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports. +- middleware-scope (REASONED; middleware-min, docs): Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it. +- host-allowlist (REASONED; docs): Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules. +- cors (REASONED; docs): Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin. +- middleware-disable (REASONED; docs): Do not use --disable-security-middleware outside a test. +- tls (REASONED; docs, nginx, caddy): There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname. +- client-tls (REASONED; docs): Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production. +- auth-app (REASONED; docs): Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label. +- csrf-key (REASONED; docs): Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control. +- env-file (REASONED; docs, source): Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching. +- key-exposure (REASONED; source, docs): Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure. +- admin-bootstrap (REASONED; docs): No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply. +- default-permission (REASONED; docs): default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant. +- auth-database (REASONED; docs): database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example. +- auth-config (REASONED; docs): MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic. +- password-rotation (REASONED; docs, curl): PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping. +- user-creation (REASONED; docs): User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user. +- client-credentials (REASONED; docs): ~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD. +- rate-limit (REASONED; docs): The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS. +- mfa (REASONED; docs, cf-docs, oauth2-proxy-source, authelia-source): MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination. +- proxy-token (REASONED; docs, source, cf-docs): MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together. +- artifact-proxy (REASONED; docs): --serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none. +- artifact-existing (REASONED; docs): Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly. +- artifact-direct (REASONED; docs): With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets. +- verify-bind (REASONED; source, iproute2): Read every listener and require 5000 on 127.0.0.1. +- verify-network (REASONED; curl, docs): Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive. +- verify-public-auth (REASONED; docs, curl): POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass. +- verify-backend-auth (REASONED; docs): Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement. +- verify-authorization (REASONED; docs): Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "ee30ff570fece0f829e8b7a87b1a777387f0b1f32d2e160049713119d344088e", + "components": { + "source": { + "name": "MLflow pinned source", + "basis": "v3.16.1", + "sources": { + "sf7285d252e29": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/cli_args.py#L162-L180", + "sa3993a9b625f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L369-L541", + "sd775f10c4d07": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/auth/__init__.py#L6008-L6021", + "s349a81b9478e": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/__init__.py#L401-L402", + "s99d30948902f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L85-L86", + "s687f1c9977cd": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L94-L105", + "s1661739df39d": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/environment_variables.py#L493-L503", + "s5801c6ac1ac2": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/rest_utils.py#L254-L292" + } + }, + "middleware-min": { + "name": "MLflow security middleware minimum", + "basis": "3.5.0", + "sources": { + "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server" + } + }, + "docs": { + "name": "MLflow documentation", + "basis": "unknown", + "sources": { + "scbbc07258943": "https://mlflow.org/docs/latest/self-hosting/security/basic-http-auth/", + "sc3a2d70cabf9": "https://mlflow.org/docs/latest/api_reference/auth/rest-api.html", + "s5b79d1871e0b": "https://mlflow.org/docs/latest/api_reference/cli.html", + "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server", + "s996e587e947a": "https://mlflow.org/docs/latest/api_reference/rest-api.html", + "s559e7a8271b9": "https://mlflow.org/docs/latest/api_reference/cli.html#mlflow" + } + }, + "docker": { + "name": "Docker Engine loopback publication minimum", + "basis": "28.0.0", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "nginx": { + "name": "nginx proxy implementation", + "basis": "release-1.28.0", + "sources": { + "s6dfa81e36717": "https://github.com/nginx/nginx/blob/release-1.28.0/src/http/modules/ngx_http_proxy_module.c#L305-L310" + } + }, + "caddy": { + "name": "Caddy reverse proxy parser", + "basis": "v2.11.4", + "sources": { + "s52039756d4f4": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L40", + "s9dde4a2bda4c": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L58-L62" + } + }, + "cf-docs": { + "name": "Cloudflare 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", + "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", + "sbec4996a7089": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33", + "s543b1f2eb29c": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49", + "sc565f2d80e6e": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59" + } + }, + "oauth2-proxy-source": { + "name": "oauth2-proxy documentation", + "basis": "v7.15.4", + "sources": { + "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", + "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" + } + }, + "authelia-source": { + "name": "Authelia source", + "basis": "v4.39.28", + "sources": { + "s768b165b44a0": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507", + "sd4dd76e70475": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.12.0", + "sources": { + "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", + "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", + "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", + "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375" + } + } + }, + "claims": { + "default-host": {"text": "v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, + "default-port": {"text": "v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, + "default-auth": {"text": "Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s2517159af609"], "status": "REASONED"}, + "container-bind": {"text": "Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports.", "components": ["docker", "docs"], "sources": ["docker:s1e53417c513d", "docs:s5b79d1871e0b"], "status": "REASONED"}, + "middleware-scope": {"text": "Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it.", "components": ["middleware-min", "docs"], "sources": ["middleware-min:s2517159af609", "docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "host-allowlist": {"text": "Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "cors": {"text": "Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "middleware-disable": {"text": "Do not use --disable-security-middleware outside a test.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "tls": {"text": "There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname.", "components": ["docs", "nginx", "caddy"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609", "nginx:s6dfa81e36717", "caddy:s52039756d4f4", "caddy:s9dde4a2bda4c"], "status": "REASONED"}, + "client-tls": {"text": "Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "auth-app": {"text": "Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "csrf-key": {"text": "Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "env-file": {"text": "Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching.", "components": ["docs", "source"], "sources": ["docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, + "key-exposure": {"text": "Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure.", "components": ["source", "docs"], "sources": ["source:sd775f10c4d07", "source:s349a81b9478e", "docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, + "admin-bootstrap": {"text": "No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "default-permission": {"text": "default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "auth-database": {"text": "database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "auth-config": {"text": "MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "password-rotation": {"text": "PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping.", "components": ["docs", "curl"], "sources": ["docs:sc3a2d70cabf9", "curl:s2b2686afaf41"], "status": "REASONED"}, + "user-creation": {"text": "User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:sc3a2d70cabf9"], "status": "REASONED"}, + "client-credentials": {"text": "~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "rate-limit": {"text": "The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "mfa": {"text": "MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination.", "components": ["docs", "cf-docs", "oauth2-proxy-source", "authelia-source"], "sources": ["docs:scbbc07258943", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "oauth2-proxy-source:sa403e3d826b4", "oauth2-proxy-source:se7a200583def", "authelia-source:s768b165b44a0", "authelia-source:sd4dd76e70475"], "status": "REASONED"}, + "proxy-token": {"text": "MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together.", "components": ["docs", "source", "cf-docs"], "sources": ["docs:s2517159af609", "source:s1661739df39d", "source:s5801c6ac1ac2", "cf-docs:sbec4996a7089", "cf-docs:s543b1f2eb29c", "cf-docs:sc565f2d80e6e"], "status": "REASONED"}, + "artifact-proxy": {"text": "--serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "artifact-existing": {"text": "Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "artifact-direct": {"text": "With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "verify-bind": {"text": "Read every listener and require 5000 on 127.0.0.1.", "components": ["source", "iproute2"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850"], "status": "REASONED", "verify": [1]}, + "verify-network": {"text": "Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive.", "components": ["curl", "docs"], "sources": ["curl:s2b2686afaf41", "docs:s5b79d1871e0b"], "status": "REASONED", "verify": [1]}, + "verify-public-auth": {"text": "POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass.", "components": ["docs", "curl"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-backend-auth": {"text": "Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s5b79d1871e0b", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]}, + "verify-authorization": {"text": "Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/nextjs.md b/plugin/skills/secure-deployment-config/references/nextjs.md index 90f1b8e6d..eea3b0a01 100644 --- a/plugin/skills/secure-deployment-config/references/nextjs.md +++ b/plugin/skills/secure-deployment-config/references/nextjs.md @@ -1,187 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "3bdad54327114894a6f45d5d5adb334b0b4fbc9bf784629b0385e6e2c4063461", - "components": { - "next": { - "name": "Next.js documentation", - "basis": "unknown", - "sources": { - "s066f8a176a52": "https://nextjs.org/docs/app/api-reference/cli/next", - "scd5749a71a79": "https://nextjs.org/docs/app/api-reference/file-conventions/proxy", - "sc2a39ce10fe1": "https://nextjs.org/docs/app/api-reference/file-conventions/route", - "sfbc4364fb5cc": "https://nextjs.org/docs/app/api-reference/functions/cookies", - "sa45045bebccb": "https://nextjs.org/docs/app/guides/authentication", - "s23a087193901": "https://nextjs.org/docs/app/guides/data-security", - "s964651d4eed4": "https://nextjs.org/docs/app/guides/environment-variables", - "s353c30dff99f": "https://nextjs.org/docs/app/guides/self-hosting" - } - }, - "next-pin": { - "name": "Next.js listener source", - "basis": "v16.3.6", - "sources": { - "s11ffc231f0c3": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246", - "s3b0feef8ec0f": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/bin/next.ts#L436-L458", - "s0ef9150d51e4": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L44", - "sefdefa3425c6": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L84-L87", - "s66bd2ed9c346": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L187-L196", - "s2b4479b3c1d2": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L309" - } - }, - "node": { - "name": "Node.js listener source", - "basis": "v22.22.1", - "sources": { - "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" - } - }, - "commander": { - "name": "Commander", - "basis": "12.1.0", - "sources": { - "s408eb603ae2e": "https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205" - } - }, - "authjs": { - "name": "Auth.js", - "basis": "unknown", - "sources": { - "sb4ac5f50ec66": "https://authjs.dev/getting-started/deployment", - "s42365460cb4c": "https://authjs.dev/getting-started/installation", - "s46bb80456d4f": "https://authjs.dev/getting-started/session-management/protecting" - } - }, - "better": { - "name": "Better Auth", - "basis": "unknown", - "sources": { - "s981751bcb554": "https://better-auth.com/docs/installation", - "s2ac2e0926f25": "https://better-auth.com/docs/integrations/next", - "sdd3d825cc3a4": "https://better-auth.com/docs/introduction", - "s8677a74455a1": "https://better-auth.com/docs/plugins/2fa", - "s4b3a514c43d9": "https://better-auth.com/docs/reference/options" - } - }, - "vercel": { - "name": "Vercel documentation", - "basis": "unknown", - "sources": { - "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", - "se6089d5c4e96": "https://vercel.com/docs/deployment-protection" - } - }, - "curl": { - "name": "curl", - "basis": "unknown", - "sources": { - "s5345a985007a": "https://curl.se/docs/manpage.html#-H" - } - }, - "grep": { - "name": "GNU grep", - "basis": "unknown", - "sources": { - "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", - "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", - "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" - } - } - }, - "claims": { - "entrypoints": {"text": "Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sc2a39ce10fe1", "next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "start-default": {"text": "v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined.", "components": ["next-pin", "node", "commander"], "sources": ["next-pin:s3b0feef8ec0f", "next-pin:s0ef9150d51e4", "next-pin:sefdefa3425c6", "next-pin:s66bd2ed9c346", "next-pin:s2b4479b3c1d2", "node:sfb3e306c102d", "commander:s408eb603ae2e", "next-pin:s11ffc231f0c3"], "status": "REASONED"}, - "private-tls": {"text": "Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED"}, - "dal": {"text": "Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "layouts": {"text": "Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "proxy": {"text": "Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sa45045bebccb"], "status": "REASONED"}, - "role": {"text": "Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, - "session-secret": {"text": "Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s964651d4eed4"], "status": "REASONED"}, - "session-jwt": {"text": "jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED"}, - "session-cookie": {"text": "Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session.", "components": ["next"], "sources": ["next:sfbc4364fb5cc", "next:sa45045bebccb"], "status": "REASONED"}, - "public-env": {"text": "NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files.", "components": ["next"], "sources": ["next:s964651d4eed4", "next:s23a087193901"], "status": "REASONED"}, - "action-key": {"text": "Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection.", "components": ["next"], "sources": ["next:s353c30dff99f", "next:s23a087193901"], "status": "REASONED"}, - "action-origin": {"text": "Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins.", "components": ["next"], "sources": ["next:s23a087193901", "next:s353c30dff99f"], "status": "REASONED"}, - "authjs-secret": {"text": "AUTH_SECRET is mandatory; npx auth secret writes .env.local.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, - "authjs-providers": {"text": "Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, - "authjs-trust": {"text": "Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66"], "status": "REASONED"}, - "authjs-route": {"text": "auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, - "authjs-proxy": {"text": "auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, - "better-secret": {"text": "BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference.", "components": ["better"], "sources": ["better:s981751bcb554", "better:s4b3a514c43d9"], "status": "REASONED"}, - "better-origins": {"text": "trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, - "better-password": {"text": "Better Auth emailAndPassword defaults disabled; enable explicitly when used.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, - "better-next": {"text": "nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, - "better-session": {"text": "Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, - "better-mfa": {"text": "twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks.", "components": ["better"], "sources": ["better:s8677a74455a1"], "status": "REASONED"}, - "vercel-methods": {"text": "Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, - "vercel-scope": {"text": "Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby.", "components": ["vercel"], "sources": ["vercel:s7836e043434a", "vercel:se6089d5c4e96"], "status": "REASONED"}, - "verify-bind": {"text": "ss inventories loopback 3000; it does not prove firewall/platform ingress isolation.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED", "verify": [1]}, - "verify-anonymous": {"text": "Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, - "verify-authorized": {"text": "Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, - "verify-cookie-input": {"text": "Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes.", "components": ["curl"], "sources": ["curl:s5345a985007a"], "status": "REASONED", "verify": [1]}, - "verify-secret": {"text": "Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape.", "components": ["next", "grep"], "sources": ["next:s964651d4eed4", "grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae"], "status": "REASONED", "verify": [1]}, - "verify-action": {"text": "Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED", "verify": [1]} - } -} ---- # Next.js: authentication that actually gates Route Handlers, Server Actions, and data - -**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 Next.js listener source v16.3.6, Node.js listener source v22.22.1, and Commander 12.1.0; 6 further sources carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 32 claims, all reasoned from cited sources. -Components (recorded basis): - -- next: Next.js documentation (unknown) -- next-pin: Next.js listener source (v16.3.6) -- node: Node.js listener source (v22.22.1) -- commander: Commander (12.1.0) -- authjs: Auth.js (unknown) -- better: Better Auth (unknown) -- vercel: Vercel documentation (unknown) -- curl: curl (unknown) -- grep: GNU grep (unknown) - -Claims (full source mapping in the front matter): - -- entrypoints (REASONED; next): Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login. -- start-default (REASONED; next-pin, node, commander): v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined. -- private-tls (REASONED; next): Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform. -- dal (REASONED; next): Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields. -- layouts (REASONED; next): Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions. -- proxy (REASONED; next): Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes. -- role (REASONED; next): Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses. -- session-secret (REASONED; next): Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_. -- session-jwt (REASONED; next): jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions. -- session-cookie (REASONED; next): Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session. -- public-env (REASONED; next): NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files. -- action-key (REASONED; next): Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection. -- action-origin (REASONED; next): Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins. -- authjs-secret (REASONED; authjs): AUTH_SECRET is mandatory; npx auth secret writes .env.local. -- authjs-providers (REASONED; authjs): Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks. -- authjs-trust (REASONED; authjs): Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically. -- authjs-route (REASONED; authjs): auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data. -- authjs-proxy (REASONED; authjs): auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider. -- better-secret (REASONED; better): BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference. -- better-origins (REASONED; better): trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks. -- better-password (REASONED; better): Better Auth emailAndPassword defaults disabled; enable explicitly when used. -- better-next (REASONED; better): nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes. -- better-session (REASONED; better): Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing. -- better-mfa (REASONED; better): twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks. -- vercel-methods (REASONED; vercel): Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication. -- vercel-scope (REASONED; vercel): Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby. -- verify-bind (REASONED; next): ss inventories loopback 3000; it does not prove firewall/platform ingress isolation. -- verify-anonymous (REASONED; next): Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies. -- verify-authorized (REASONED; next): Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success. -- verify-cookie-input (REASONED; curl): Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes. -- verify-secret (REASONED; next, grep): Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape. -- verify-action (REASONED; next): Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found. - +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 Next.js app has many entry points: pages, layouts, Route Handlers (`app/**/route.ts`), and every exported Server Action, which the Next.js docs describe as reachable by a direct POST whether or not your UI calls it. A check that lives only in a layout or in `proxy.ts` (the file Next.js 16 renamed from `middleware.ts`) leaves open the entry points it does not cover (a layout guards no Route Handler or Server Action; a `proxy.ts` matcher guards only the paths it matches), and authorization still belongs next to the data. Next.js supplies cookies and sessions as primitives, not a login system; the login is yours or a library's. @@ -422,3 +243,208 @@ curl -q -sS --noproxy '*' -o dash-nocookie.txt -w 'http=%{http_code} loc=%{redi - Commander 12.1.0 (the version Next.js v16.3.6 pins): an option that takes a value is `undefined` unless specified on the command line: https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205 - Next.js v16.3.6 pins `commander` at `12.1.0` (pinned tag v16.3.6): https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246 - curl `--header @-` (header lines read from stdin): https://curl.se/docs/manpage.html#-H ; GNU grep `-f -` ("When file is '-', read patterns from standard input"), `-l` ("print the name of each input file from which output would normally have been printed") and its exit status (0 if a line is selected, 1 if none, 2 on error): https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html , https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html and https://www.gnu.org/software/grep/manual/html_node/Exit-Status.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/nextjs.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): + +- next-pin: Next.js listener source (v16.3.6) +- next: Next.js documentation (unknown) +- node: Node.js listener source (v22.22.1) +- commander: Commander (12.1.0) +- authjs: Auth.js (unknown) +- better: Better Auth (unknown) +- vercel: Vercel documentation (unknown) +- curl: curl (unknown) +- grep: GNU grep (unknown) + +Claims (full source mapping in the JSON block below): + +- entrypoints (REASONED; next): Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login. +- start-default (REASONED; next-pin, node, commander): v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined. +- private-tls (REASONED; next): Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform. +- dal (REASONED; next): Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields. +- layouts (REASONED; next): Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions. +- proxy (REASONED; next): Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes. +- role (REASONED; next): Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses. +- session-secret (REASONED; next): Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_. +- session-jwt (REASONED; next): jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions. +- session-cookie (REASONED; next): Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session. +- public-env (REASONED; next): NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files. +- action-key (REASONED; next): Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection. +- action-origin (REASONED; next): Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins. +- authjs-secret (REASONED; authjs): AUTH_SECRET is mandatory; npx auth secret writes .env.local. +- authjs-providers (REASONED; authjs): Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks. +- authjs-trust (REASONED; authjs): Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically. +- authjs-route (REASONED; authjs): auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data. +- authjs-proxy (REASONED; authjs): auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider. +- better-secret (REASONED; better): BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference. +- better-origins (REASONED; better): trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks. +- better-password (REASONED; better): Better Auth emailAndPassword defaults disabled; enable explicitly when used. +- better-next (REASONED; better): nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes. +- better-session (REASONED; better): Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing. +- better-mfa (REASONED; better): twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks. +- vercel-methods (REASONED; vercel): Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication. +- vercel-scope (REASONED; vercel): Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby. +- verify-bind (REASONED; next): ss inventories loopback 3000; it does not prove firewall/platform ingress isolation. +- verify-anonymous (REASONED; next): Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies. +- verify-authorized (REASONED; next): Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success. +- verify-cookie-input (REASONED; curl): Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes. +- verify-secret (REASONED; next, grep): Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape. +- verify-action (REASONED; next): Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "3bdad54327114894a6f45d5d5adb334b0b4fbc9bf784629b0385e6e2c4063461", + "components": { + "next-pin": { + "name": "Next.js listener source", + "basis": "v16.3.6", + "sources": { + "s11ffc231f0c3": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246", + "s3b0feef8ec0f": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/bin/next.ts#L436-L458", + "s0ef9150d51e4": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L44", + "sefdefa3425c6": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L84-L87", + "s66bd2ed9c346": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L187-L196", + "s2b4479b3c1d2": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L309" + } + }, + "next": { + "name": "Next.js documentation", + "basis": "unknown", + "sources": { + "s066f8a176a52": "https://nextjs.org/docs/app/api-reference/cli/next", + "scd5749a71a79": "https://nextjs.org/docs/app/api-reference/file-conventions/proxy", + "sc2a39ce10fe1": "https://nextjs.org/docs/app/api-reference/file-conventions/route", + "sfbc4364fb5cc": "https://nextjs.org/docs/app/api-reference/functions/cookies", + "sa45045bebccb": "https://nextjs.org/docs/app/guides/authentication", + "s23a087193901": "https://nextjs.org/docs/app/guides/data-security", + "s964651d4eed4": "https://nextjs.org/docs/app/guides/environment-variables", + "s353c30dff99f": "https://nextjs.org/docs/app/guides/self-hosting" + } + }, + "node": { + "name": "Node.js listener source", + "basis": "v22.22.1", + "sources": { + "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" + } + }, + "commander": { + "name": "Commander", + "basis": "12.1.0", + "sources": { + "s408eb603ae2e": "https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205" + } + }, + "authjs": { + "name": "Auth.js", + "basis": "unknown", + "sources": { + "sb4ac5f50ec66": "https://authjs.dev/getting-started/deployment", + "s42365460cb4c": "https://authjs.dev/getting-started/installation", + "s46bb80456d4f": "https://authjs.dev/getting-started/session-management/protecting" + } + }, + "better": { + "name": "Better Auth", + "basis": "unknown", + "sources": { + "s981751bcb554": "https://better-auth.com/docs/installation", + "s2ac2e0926f25": "https://better-auth.com/docs/integrations/next", + "sdd3d825cc3a4": "https://better-auth.com/docs/introduction", + "s8677a74455a1": "https://better-auth.com/docs/plugins/2fa", + "s4b3a514c43d9": "https://better-auth.com/docs/reference/options" + } + }, + "vercel": { + "name": "Vercel documentation", + "basis": "unknown", + "sources": { + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection" + } + }, + "curl": { + "name": "curl", + "basis": "unknown", + "sources": { + "s5345a985007a": "https://curl.se/docs/manpage.html#-H" + } + }, + "grep": { + "name": "GNU grep", + "basis": "unknown", + "sources": { + "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", + "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", + "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" + } + } + }, + "claims": { + "entrypoints": {"text": "Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sc2a39ce10fe1", "next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "start-default": {"text": "v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined.", "components": ["next-pin", "node", "commander"], "sources": ["next-pin:s3b0feef8ec0f", "next-pin:s0ef9150d51e4", "next-pin:sefdefa3425c6", "next-pin:s66bd2ed9c346", "next-pin:s2b4479b3c1d2", "node:sfb3e306c102d", "commander:s408eb603ae2e", "next-pin:s11ffc231f0c3"], "status": "REASONED"}, + "private-tls": {"text": "Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED"}, + "dal": {"text": "Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "layouts": {"text": "Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "proxy": {"text": "Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sa45045bebccb"], "status": "REASONED"}, + "role": {"text": "Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "session-secret": {"text": "Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s964651d4eed4"], "status": "REASONED"}, + "session-jwt": {"text": "jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED"}, + "session-cookie": {"text": "Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session.", "components": ["next"], "sources": ["next:sfbc4364fb5cc", "next:sa45045bebccb"], "status": "REASONED"}, + "public-env": {"text": "NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files.", "components": ["next"], "sources": ["next:s964651d4eed4", "next:s23a087193901"], "status": "REASONED"}, + "action-key": {"text": "Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection.", "components": ["next"], "sources": ["next:s353c30dff99f", "next:s23a087193901"], "status": "REASONED"}, + "action-origin": {"text": "Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins.", "components": ["next"], "sources": ["next:s23a087193901", "next:s353c30dff99f"], "status": "REASONED"}, + "authjs-secret": {"text": "AUTH_SECRET is mandatory; npx auth secret writes .env.local.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, + "authjs-providers": {"text": "Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, + "authjs-trust": {"text": "Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66"], "status": "REASONED"}, + "authjs-route": {"text": "auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, + "authjs-proxy": {"text": "auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, + "better-secret": {"text": "BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference.", "components": ["better"], "sources": ["better:s981751bcb554", "better:s4b3a514c43d9"], "status": "REASONED"}, + "better-origins": {"text": "trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, + "better-password": {"text": "Better Auth emailAndPassword defaults disabled; enable explicitly when used.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, + "better-next": {"text": "nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, + "better-session": {"text": "Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, + "better-mfa": {"text": "twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks.", "components": ["better"], "sources": ["better:s8677a74455a1"], "status": "REASONED"}, + "vercel-methods": {"text": "Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "vercel-scope": {"text": "Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby.", "components": ["vercel"], "sources": ["vercel:s7836e043434a", "vercel:se6089d5c4e96"], "status": "REASONED"}, + "verify-bind": {"text": "ss inventories loopback 3000; it does not prove firewall/platform ingress isolation.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED", "verify": [1]}, + "verify-anonymous": {"text": "Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, + "verify-authorized": {"text": "Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, + "verify-cookie-input": {"text": "Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes.", "components": ["curl"], "sources": ["curl:s5345a985007a"], "status": "REASONED", "verify": [1]}, + "verify-secret": {"text": "Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape.", "components": ["next", "grep"], "sources": ["next:s964651d4eed4", "grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae"], "status": "REASONED", "verify": [1]}, + "verify-action": {"text": "Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/paas.md b/plugin/skills/secure-deployment-config/references/paas.md index 2f707d2ab..a3f7a14bc 100644 --- a/plugin/skills/secure-deployment-config/references/paas.md +++ b/plugin/skills/secure-deployment-config/references/paas.md @@ -1,214 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-19", - "documentation_checked": "2026-09", - "body_sha256": "2db22068c994d124ee04bbb96560a48925d4f295f118e343bc0e18ff0d9155b5", - "components": { - "render": { - "name": "Render documentation", - "basis": "unknown", - "sources": { - "s213783ba6209": "https://render.com/docs", - "se240afec8d2d": "https://render.com/docs/postgresql-creating-connecting", - "s1820ac471b6c": "https://render.com/docs/web-services", - "s5b8400c2a2ba": "https://render.com/docs/tls", - "s22963948fc90": "https://render.com/docs/custom-domains" - } - }, - "fly": { - "name": "Fly.io documentation", - "basis": "unknown", - "sources": { - "s1b202c527f2e": "https://fly.io/docs" - } - }, - "vercel": { - "name": "Vercel documentation", - "basis": "unknown", - "sources": { - "s2b470a6bc853": "https://vercel.com/docs", - "se6089d5c4e96": "https://vercel.com/docs/deployment-protection", - "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", - "s3e7832fd3031": "https://vercel.com/docs/cdn-security/encryption", - "se314aa4b7f1e": "https://vercel.com/docs/domains/working-with-ssl", - "s22498401f5d0": "https://vercel.com/docs/functions/runtimes", - "s845513031876": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection", - "sc1588029e14e": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation" - } - }, - "spaces": { - "name": "Hugging Face Spaces documentation", - "basis": "unknown", - "sources": { - "sda0c810c053d": "https://huggingface.co/docs/hub/spaces-overview", - "sd5ec2b2772e0": "https://huggingface.co/docs/hub/spaces-config-reference" - } - }, - "nextjs": { - "name": "Next.js environment variables", - "basis": "15", - "sources": { - "s22e4b70dab8b": "https://nextjs.org/docs/15/app/guides/environment-variables" - } - }, - "express": { - "name": "Express request protocol", - "basis": "5.x", - "sources": { - "s5bc62e66824b": "https://expressjs.com/en/5x/api/request/#req.protocol" - } - }, - "django": { - "name": "Django proxy security setting", - "basis": "6.1", - "sources": { - "sb5e26f65fd91": "https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header" - } - }, - "mysql-client": { - "name": "MySQL client connection options", - "basis": "8.4", - "sources": { - "s823c0bfaaafa": "https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode" - } - }, - "spaces-pinned": { - "name": "Hugging Face Hub documentation", - "basis": "6fb324369143cb76a723c88a592010514ac1095a", - "sources": { - "se2fb4e2a5c02": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45", - "s9d332f0940e2": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118", - "s969a28a4c44e": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27", - "s5d393b019bad": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14", - "sd1ae2b703a70": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78" - } - }, - "heroku": { - "name": "Heroku documentation", - "basis": "unknown", - "sources": { - "sa86eb06bbd4a": "https://devcenter.heroku.com/articles/automated-certificate-management", - "s0c5bf300cef4": "https://devcenter.heroku.com/articles/http-routing", - "sfbcf716168be": "https://devcenter.heroku.com/articles/custom-domains", - "s370d4beec29a": "https://devcenter.heroku.com/articles/runtime-principles", - "s211f5fe76042": "https://devcenter.heroku.com/articles/generations" - } - }, - "fly-pinned": { - "name": "Fly.io pinned documentation", - "basis": "2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9", - "sources": { - "s254c8c8a5edd": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97", - "s32999aa4c58c": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176", - "sf4105f79b22b": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44", - "sea5560c14d13": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87", - "sb935e8478a30": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102", - "s62fec91543a1": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42", - "s61c1e50d2c11": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92", - "s6e9a7292860d": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7", - "sa63b828c93df": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350", - "s4324a02dbc42": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229", - "scf2e6f3bce02": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123" - } - }, - "railway": { - "name": "Railway documentation", - "basis": "72354493198302677fb6be6c692dd75c64cae2c6", - "sources": { - "s93da6ccbfe58": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122", - "s6d4dc8fb7784": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247", - "s2bc94f13536b": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150", - "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146", - "sbf08758c106a": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17", - "s844d338bd121": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38" - } - }, - "nextjs-pinned": { - "name": "Next.js pinned documentation", - "basis": "v16.4.2", - "sources": { - "s0e3d2cd2248e": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164", - "sf7cfdc553311": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" - } - } - }, - "claims": { - "managed-tls": {"text": "TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "render:s5b8400c2a2ba", "render:s22963948fc90", "vercel:s3e7832fd3031", "vercel:se314aa4b7f1e", "heroku:sa86eb06bbd4a", "heroku:s0c5bf300cef4", "heroku:sfbcf716168be", "fly-pinned:s254c8c8a5edd", "fly-pinned:s32999aa4c58c", "fly-pinned:sf4105f79b22b", "fly-pinned:sea5560c14d13", "fly-pinned:sb935e8478a30", "railway:s93da6ccbfe58", "railway:s6d4dc8fb7784", "railway:s2bc94f13536b"], "status": "REASONED"}, - "application-auth": {"text": "Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, - "vercel-login": {"text": "Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s7836e043434a"], "status": "REASONED"}, - "vercel-password": {"text": "At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, - "secret-store": {"text": "Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles.", "components": ["render", "fly", "vercel"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853"], "status": "REASONED"}, - "public-env": {"text": "Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_.", "components": ["vercel", "nextjs"], "sources": ["vercel:s2b470a6bc853", "nextjs:s22e4b70dab8b"], "status": "REASONED"}, - "https-redirect": {"text": "Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s5b8400c2a2ba", "vercel:s3e7832fd3031", "heroku:s0c5bf300cef4", "express:s5bc62e66824b", "django:sb5e26f65fd91", "fly-pinned:s6e9a7292860d", "fly-pinned:sa63b828c93df", "fly-pinned:s4324a02dbc42", "fly-pinned:scf2e6f3bce02", "railway:sbf08758c106a", "railway:s844d338bd121"], "status": "REASONED"}, - "proxy-awareness": {"text": "Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform.", "components": ["render", "fly", "vercel", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "express:s5bc62e66824b", "django:sb5e26f65fd91"], "status": "REASONED"}, - "listener-port": {"text": "Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "vercel:s22498401f5d0", "heroku:s370d4beec29a", "heroku:s211f5fe76042", "fly-pinned:s62fec91543a1", "fly-pinned:s61c1e50d2c11", "railway:sac1383056b65"], "status": "REASONED"}, - "database-identity": {"text": "Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY.", "components": ["render", "mysql-client"], "sources": ["render:se240afec8d2d", "mysql-client:s823c0bfaaafa"], "status": "REASONED"}, - "render-internal-tls": {"text": "Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, - "database-network": {"text": "Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, - "spaces-creation": {"text": "This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:se2fb4e2a5c02", "spaces-pinned:s9d332f0940e2", "spaces-pinned:s969a28a4c44e"], "status": "REASONED"}, - "spaces-public": {"text": "Public Spaces expose both source and running app.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-protected": {"text": "Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-private": {"text": "Private Spaces restrict both source and running app to the owner and collaborators.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-secrets": {"text": "Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d", "spaces:sd5ec2b2772e0"], "status": "REASONED"}, - "spaces-static": {"text": "Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, - "spaces-dev": {"text": "At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:s5d393b019bad", "spaces-pinned:sd1ae2b703a70"], "status": "REASONED"}, - "verify-redirect": {"text": "This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness.", "components": ["vercel", "render", "fly-pinned", "railway", "heroku"], "sources": ["vercel:s2b470a6bc853", "vercel:s3e7832fd3031", "render:s5b8400c2a2ba", "fly-pinned:s4324a02dbc42", "railway:sbf08758c106a", "railway:s844d338bd121", "heroku:s0c5bf300cef4"], "status": "REASONED", "verify": [1]}, - "verify-app-auth": {"text": "Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s845513031876", "vercel:sc1588029e14e"], "status": "REASONED", "verify": [1]}, - "verify-secrets": {"text": "Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan.", "components": ["render", "fly", "vercel", "nextjs-pinned"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "nextjs-pinned:s0e3d2cd2248e", "nextjs-pinned:sf7cfdc553311"], "status": "REASONED", "verify": [1]} - } -} ---- # PaaS platforms: what the platform does, what stays yours - -**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): - -- render: Render documentation (unknown) -- fly: Fly.io documentation (unknown) -- vercel: Vercel documentation (unknown) -- spaces: Hugging Face Spaces documentation (unknown) -- nextjs: Next.js environment variables (15) -- express: Express request protocol (5.x) -- django: Django proxy security setting (6.1) -- mysql-client: MySQL client connection options (8.4) -- spaces-pinned: Hugging Face Hub documentation (6fb324369143cb76a723c88a592010514ac1095a) -- heroku: Heroku documentation (unknown) -- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) -- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) -- nextjs-pinned: Next.js pinned documentation (v16.4.2) +Written against Render documentation (no recorded version), Next.js pinned documentation v16.4.2, Next.js environment variables 15, Express request protocol 5.x, Django proxy security setting 6.1, and MySQL client connection options 8.4; 3 further sources are pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 22 claims, all reasoned from cited sources. -Claims (full source mapping in the front matter): - -- managed-tls (REASONED; render, fly, vercel, heroku, fly-pinned, railway): TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate. -- application-auth (REASONED; vercel): Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls. -- vercel-login (REASONED; vercel): Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby. -- vercel-password (REASONED; vercel): At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise. -- secret-store (REASONED; render, fly, vercel): Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles. -- public-env (REASONED; vercel, nextjs): Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_. -- https-redirect (REASONED; render, fly, vercel, heroku, fly-pinned, railway, express, django): Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization. -- proxy-awareness (REASONED; render, fly, vercel, express, django): Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform. -- listener-port (REASONED; render, fly, vercel, heroku, fly-pinned, railway): Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners. -- database-identity (REASONED; render, mysql-client): Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY. -- render-internal-tls (REASONED; render): Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification. -- database-network (REASONED; render): Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store. -- spaces-creation (REASONED; spaces, spaces-pinned): This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only. -- spaces-public (REASONED; spaces): Public Spaces expose both source and running app. -- spaces-protected (REASONED; spaces): Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations. -- spaces-private (REASONED; spaces): Private Spaces restrict both source and running app to the owner and collaborators. -- spaces-secrets (REASONED; spaces): Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable. -- spaces-static (REASONED; spaces): Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service. -- spaces-dev (REASONED; spaces, spaces-pinned): At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path. -- verify-redirect (REASONED; vercel, render, fly-pinned, railway, heroku): This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness. -- verify-app-auth (REASONED; vercel): Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement. -- verify-secrets (REASONED; render, fly, vercel, nextjs-pinned): Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan. - +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 responsibilities depend on the platform and deployment mode: @@ -332,3 +126,235 @@ curl -q -g -s --noproxy '*' -o /dev/null -w 'http=%{http_code}\n' https://app.ex - Fly HTTP handler forwarded-protocol header (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123 - Railway public-networking headers and request handling (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17 - Railway GET redirect status and POST conversion (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38 + + + +## 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/paas.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): + +- render: Render documentation (unknown) +- fly: Fly.io documentation (unknown) +- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) +- vercel: Vercel documentation (unknown) +- spaces: Hugging Face Spaces documentation (unknown) +- spaces-pinned: Hugging Face Hub documentation (6fb324369143cb76a723c88a592010514ac1095a) +- heroku: Heroku documentation (unknown) +- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) +- nextjs-pinned: Next.js pinned documentation (v16.4.2) +- nextjs: Next.js environment variables (15) +- express: Express request protocol (5.x) +- django: Django proxy security setting (6.1) +- mysql-client: MySQL client connection options (8.4) + +Claims (full source mapping in the JSON block below): + +- managed-tls (REASONED; render, fly, vercel, heroku, fly-pinned, railway): TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate. +- application-auth (REASONED; vercel): Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls. +- vercel-login (REASONED; vercel): Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby. +- vercel-password (REASONED; vercel): At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise. +- secret-store (REASONED; render, fly, vercel): Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles. +- public-env (REASONED; vercel, nextjs): Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_. +- https-redirect (REASONED; render, fly, vercel, heroku, fly-pinned, railway, express, django): Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization. +- proxy-awareness (REASONED; render, fly, vercel, express, django): Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform. +- listener-port (REASONED; render, fly, vercel, heroku, fly-pinned, railway): Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners. +- database-identity (REASONED; render, mysql-client): Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY. +- render-internal-tls (REASONED; render): Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification. +- database-network (REASONED; render): Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store. +- spaces-creation (REASONED; spaces, spaces-pinned): This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only. +- spaces-public (REASONED; spaces): Public Spaces expose both source and running app. +- spaces-protected (REASONED; spaces): Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations. +- spaces-private (REASONED; spaces): Private Spaces restrict both source and running app to the owner and collaborators. +- spaces-secrets (REASONED; spaces): Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable. +- spaces-static (REASONED; spaces): Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service. +- spaces-dev (REASONED; spaces, spaces-pinned): At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path. +- verify-redirect (REASONED; vercel, render, fly-pinned, railway, heroku): This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness. +- verify-app-auth (REASONED; vercel): Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement. +- verify-secrets (REASONED; render, fly, vercel, nextjs-pinned): Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "2db22068c994d124ee04bbb96560a48925d4f295f118e343bc0e18ff0d9155b5", + "components": { + "render": { + "name": "Render documentation", + "basis": "unknown", + "sources": { + "s213783ba6209": "https://render.com/docs", + "se240afec8d2d": "https://render.com/docs/postgresql-creating-connecting", + "s1820ac471b6c": "https://render.com/docs/web-services", + "s5b8400c2a2ba": "https://render.com/docs/tls", + "s22963948fc90": "https://render.com/docs/custom-domains" + } + }, + "fly": { + "name": "Fly.io documentation", + "basis": "unknown", + "sources": { + "s1b202c527f2e": "https://fly.io/docs" + } + }, + "fly-pinned": { + "name": "Fly.io pinned documentation", + "basis": "2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9", + "sources": { + "s254c8c8a5edd": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97", + "s32999aa4c58c": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176", + "sf4105f79b22b": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44", + "sea5560c14d13": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87", + "sb935e8478a30": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102", + "s62fec91543a1": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42", + "s61c1e50d2c11": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92", + "s6e9a7292860d": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7", + "sa63b828c93df": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350", + "s4324a02dbc42": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229", + "scf2e6f3bce02": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123" + } + }, + "vercel": { + "name": "Vercel documentation", + "basis": "unknown", + "sources": { + "s2b470a6bc853": "https://vercel.com/docs", + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection", + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", + "s3e7832fd3031": "https://vercel.com/docs/cdn-security/encryption", + "se314aa4b7f1e": "https://vercel.com/docs/domains/working-with-ssl", + "s22498401f5d0": "https://vercel.com/docs/functions/runtimes", + "s845513031876": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection", + "sc1588029e14e": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation" + } + }, + "spaces": { + "name": "Hugging Face Spaces documentation", + "basis": "unknown", + "sources": { + "sda0c810c053d": "https://huggingface.co/docs/hub/spaces-overview", + "sd5ec2b2772e0": "https://huggingface.co/docs/hub/spaces-config-reference" + } + }, + "spaces-pinned": { + "name": "Hugging Face Hub documentation", + "basis": "6fb324369143cb76a723c88a592010514ac1095a", + "sources": { + "se2fb4e2a5c02": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45", + "s9d332f0940e2": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118", + "s969a28a4c44e": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27", + "s5d393b019bad": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14", + "sd1ae2b703a70": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78" + } + }, + "heroku": { + "name": "Heroku documentation", + "basis": "unknown", + "sources": { + "sa86eb06bbd4a": "https://devcenter.heroku.com/articles/automated-certificate-management", + "s0c5bf300cef4": "https://devcenter.heroku.com/articles/http-routing", + "sfbcf716168be": "https://devcenter.heroku.com/articles/custom-domains", + "s370d4beec29a": "https://devcenter.heroku.com/articles/runtime-principles", + "s211f5fe76042": "https://devcenter.heroku.com/articles/generations" + } + }, + "railway": { + "name": "Railway documentation", + "basis": "72354493198302677fb6be6c692dd75c64cae2c6", + "sources": { + "s93da6ccbfe58": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122", + "s6d4dc8fb7784": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247", + "s2bc94f13536b": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150", + "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146", + "sbf08758c106a": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17", + "s844d338bd121": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38" + } + }, + "nextjs-pinned": { + "name": "Next.js pinned documentation", + "basis": "v16.4.2", + "sources": { + "s0e3d2cd2248e": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164", + "sf7cfdc553311": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" + } + }, + "nextjs": { + "name": "Next.js environment variables", + "basis": "15", + "sources": { + "s22e4b70dab8b": "https://nextjs.org/docs/15/app/guides/environment-variables" + } + }, + "express": { + "name": "Express request protocol", + "basis": "5.x", + "sources": { + "s5bc62e66824b": "https://expressjs.com/en/5x/api/request/#req.protocol" + } + }, + "django": { + "name": "Django proxy security setting", + "basis": "6.1", + "sources": { + "sb5e26f65fd91": "https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header" + } + }, + "mysql-client": { + "name": "MySQL client connection options", + "basis": "8.4", + "sources": { + "s823c0bfaaafa": "https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode" + } + } + }, + "claims": { + "managed-tls": {"text": "TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "render:s5b8400c2a2ba", "render:s22963948fc90", "vercel:s3e7832fd3031", "vercel:se314aa4b7f1e", "heroku:sa86eb06bbd4a", "heroku:s0c5bf300cef4", "heroku:sfbcf716168be", "fly-pinned:s254c8c8a5edd", "fly-pinned:s32999aa4c58c", "fly-pinned:sf4105f79b22b", "fly-pinned:sea5560c14d13", "fly-pinned:sb935e8478a30", "railway:s93da6ccbfe58", "railway:s6d4dc8fb7784", "railway:s2bc94f13536b"], "status": "REASONED"}, + "application-auth": {"text": "Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "vercel-login": {"text": "Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s7836e043434a"], "status": "REASONED"}, + "vercel-password": {"text": "At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "secret-store": {"text": "Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles.", "components": ["render", "fly", "vercel"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853"], "status": "REASONED"}, + "public-env": {"text": "Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_.", "components": ["vercel", "nextjs"], "sources": ["vercel:s2b470a6bc853", "nextjs:s22e4b70dab8b"], "status": "REASONED"}, + "https-redirect": {"text": "Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s5b8400c2a2ba", "vercel:s3e7832fd3031", "heroku:s0c5bf300cef4", "express:s5bc62e66824b", "django:sb5e26f65fd91", "fly-pinned:s6e9a7292860d", "fly-pinned:sa63b828c93df", "fly-pinned:s4324a02dbc42", "fly-pinned:scf2e6f3bce02", "railway:sbf08758c106a", "railway:s844d338bd121"], "status": "REASONED"}, + "proxy-awareness": {"text": "Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform.", "components": ["render", "fly", "vercel", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "express:s5bc62e66824b", "django:sb5e26f65fd91"], "status": "REASONED"}, + "listener-port": {"text": "Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "vercel:s22498401f5d0", "heroku:s370d4beec29a", "heroku:s211f5fe76042", "fly-pinned:s62fec91543a1", "fly-pinned:s61c1e50d2c11", "railway:sac1383056b65"], "status": "REASONED"}, + "database-identity": {"text": "Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY.", "components": ["render", "mysql-client"], "sources": ["render:se240afec8d2d", "mysql-client:s823c0bfaaafa"], "status": "REASONED"}, + "render-internal-tls": {"text": "Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, + "database-network": {"text": "Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, + "spaces-creation": {"text": "This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:se2fb4e2a5c02", "spaces-pinned:s9d332f0940e2", "spaces-pinned:s969a28a4c44e"], "status": "REASONED"}, + "spaces-public": {"text": "Public Spaces expose both source and running app.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-protected": {"text": "Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-private": {"text": "Private Spaces restrict both source and running app to the owner and collaborators.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-secrets": {"text": "Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d", "spaces:sd5ec2b2772e0"], "status": "REASONED"}, + "spaces-static": {"text": "Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-dev": {"text": "At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:s5d393b019bad", "spaces-pinned:sd1ae2b703a70"], "status": "REASONED"}, + "verify-redirect": {"text": "This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness.", "components": ["vercel", "render", "fly-pinned", "railway", "heroku"], "sources": ["vercel:s2b470a6bc853", "vercel:s3e7832fd3031", "render:s5b8400c2a2ba", "fly-pinned:s4324a02dbc42", "railway:sbf08758c106a", "railway:s844d338bd121", "heroku:s0c5bf300cef4"], "status": "REASONED", "verify": [1]}, + "verify-app-auth": {"text": "Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s845513031876", "vercel:sc1588029e14e"], "status": "REASONED", "verify": [1]}, + "verify-secrets": {"text": "Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan.", "components": ["render", "fly", "vercel", "nextjs-pinned"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "nextjs-pinned:s0e3d2cd2248e", "nextjs-pinned:sf7cfdc553311"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/php.md b/plugin/skills/secure-deployment-config/references/php.md index ed3eab7a3..a83fd5cd8 100644 --- a/plugin/skills/secure-deployment-config/references/php.md +++ b/plugin/skills/secure-deployment-config/references/php.md @@ -1,250 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "b65232e7b90f8edc69a6b0b2639a04b3f7b6b4590b7015c4d1d84cd70d5f9aae", - "components": { - "php": { - "name": "PHP documentation", - "basis": "unknown", - "sources": { - "s09fdbeba0867": "https://www.php.net/manual/en/curl.constants.php", - "s5840a5a0a848": "https://www.php.net/manual/en/features.commandline.options.php", - "s3cbc65590d52": "https://www.php.net/manual/en/features.file-upload.common-pitfalls.php", - "s6f3e55d536e7": "https://www.php.net/manual/en/filesystem.configuration.php", - "s50f85ff4fbd1": "https://www.php.net/manual/en/function.password-hash.php", - "s625cd1d40655": "https://www.php.net/manual/en/function.session-regenerate-id.php", - "s2f1c806d037c": "https://www.php.net/manual/en/function.trigger-error.php", - "s7b2c2c19835d": "https://www.php.net/manual/en/security.hiding.php", - "s8664dc56a028": "https://www.php.net/manual/en/session.security.ini.php" - } - }, - "curl": { - "name": "curl manual", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "php8": { - "name": "PHP native configuration", - "basis": "8.x", - "sources": { - "s89c799077b82": "https://www.php.net/manual/en/errorfunc.configuration.php", - "s5a3f7bde8338": "https://www.php.net/manual/en/info.configuration.php", - "sc9e417e2ad6f": "https://www.php.net/manual/en/ini.core.php", - "s2e692b7c86a6": "https://www.php.net/manual/en/install.fpm.configuration.php", - "s000e155a7b25": "https://www.php.net/manual/en/session.configuration.php" - } - }, - "template84": { - "name": "PHP production template", - "basis": "8.4", - "sources": { - "sd72e3d69551f": "https://raw.githubusercontent.com/php/php-src/d313ad6098430f4e61f0121a9e7ab392d195e4e4/php.ini-production" - } - }, - "template83": { - "name": "PHP production template", - "basis": "8.3", - "sources": { - "sea3d3a4ea486": "https://raw.githubusercontent.com/php/php-src/b94f9f68a610e0fca5f2fea5bfa7c7d6d3d5a847/php.ini-production" - } - }, - "laravel": { - "name": "Laravel", - "basis": "12.x", - "sources": { - "sf0448f3d4665": "https://api.laravel.com/docs/12.x/Illuminate/Routing/UrlGenerator.html", - "s98db3429151c": "https://github.com/laravel/laravel/blob/f6b2e79bdbfc5bf4a37ad16466cc06ad79cc9e8f/config/session.php", - "s4e846bd588a8": "https://laravel.com/framework/docs/12.x/configuration", - "s6510c1aa8d96": "https://laravel.com/framework/docs/12.x/deployment", - "sb3e2d08bd07d": "https://laravel.com/framework/docs/12.x/encryption", - "s879d69d64852": "https://laravel.com/framework/docs/12.x/filesystem", - "sc58ba221e3cd": "https://laravel.com/framework/docs/12.x/fortify", - "sdf4ba04ddcda": "https://laravel.com/framework/docs/12.x/hashing", - "s517839d232ec": "https://laravel.com/framework/docs/12.x/requests", - "s248fef722141": "https://laravel.com/framework/docs/12.x/routing", - "s003198079bdd": "https://laravel.com/framework/docs/12.x/socialite", - "sb3e846095c54": "https://laravel.com/framework/docs/12.x/starter-kits", - "sd7d2287b5d35": "https://laravel.com/framework/docs/12.x/telescope" - } - }, - "debugbar": { - "name": "Laravel Debugbar", - "basis": "unknown", - "sources": { - "s7099781fdf4a": "https://github.com/fruitcake/laravel-debugbar" - } - }, - "fail2ban": { - "name": "fail2ban", - "basis": "1.1.1", - "sources": { - "s0470655e109b": "https://github.com/fail2ban/fail2ban/blob/1.1.1/man/jail.conf.5#L187-L200", - "s9b3b926fe020": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/jail.conf#L378-L384", - "sdb5f6f355db6": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/filter.d/nginx-limit-req.conf#L48-L49" - } - } - }, - "claims": { - "fpm-private": {"text": "FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "fpm-clients": {"text": "listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "fpm-socket": {"text": "Unix socket access uses listen.owner, listen.group and listen.mode, default 0660.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "cookie-secure": {"text": "Native session.cookie_secure defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "cookie-httponly": {"text": "Native session.cookie_httponly defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "cookie-samesite": {"text": "Native session.cookie_samesite defaults empty; choose Lax or Strict.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-strict": {"text": "session.use_strict_mode defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-regenerate": {"text": "Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion.", "components": ["php"], "sources": ["php:s625cd1d40655"], "status": "REASONED"}, - "passwords": {"text": "PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes.", "components": ["php"], "sources": ["php:s50f85ff4fbd1"], "status": "REASONED"}, - "plain-auth": {"text": "Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here.", "components": ["php", "fail2ban"], "sources": ["php:s50f85ff4fbd1", "php:s8664dc56a028", "fail2ban:s0470655e109b", "fail2ban:s9b3b926fe020", "fail2ban:sdb5f6f355db6"], "status": "REASONED"}, - "laravel-debug": {"text": "Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, - "laravel-key": {"text": "Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation.", "components": ["laravel"], "sources": ["laravel:sb3e2d08bd07d"], "status": "REASONED"}, - "laravel-secure": {"text": "Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, - "laravel-httponly": {"text": "SESSION_HTTP_ONLY defaults true; retain it.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, - "laravel-samesite": {"text": "SESSION_SAME_SITE defaults lax; strict suits admin-only apps.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, - "laravel-proxy": {"text": "Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, - "laravel-urls": {"text": "URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs.", "components": ["laravel"], "sources": ["laravel:sf0448f3d4665"], "status": "REASONED"}, - "laravel-hashes": {"text": "Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms.", "components": ["laravel"], "sources": ["laravel:sdf4ba04ddcda"], "status": "REASONED"}, - "laravel-limiter": {"text": "Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route.", "components": ["laravel"], "sources": ["laravel:s248fef722141"], "status": "REASONED"}, - "fortify-login": {"text": "12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, - "fortify-mfa": {"text": "Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, - "socialite": {"text": "Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks.", "components": ["laravel"], "sources": ["laravel:s003198079bdd"], "status": "REASONED"}, - "laravel-cache": {"text": "Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, - "laravel-hosts": {"text": "Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, - "debug-tools": {"text": "Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration.", "components": ["laravel", "debugbar"], "sources": ["laravel:sd7d2287b5d35", "debugbar:s7099781fdf4a"], "status": "REASONED"}, - "curl-peer": {"text": "CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, - "curl-host": {"text": "CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, - "display-errors": {"text": "PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "startup-errors": {"text": "display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "log-errors": {"text": "log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "error-reporting": {"text": "error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, - "php-banner": {"text": "expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s7b2c2c19835d"], "status": "REASONED"}, - "production-template": {"text": "PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled.", "components": ["template84"], "sources": ["template84:sd72e3d69551f"], "status": "REASONED"}, - "disable-functions": {"text": "disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "url-fopen": {"text": "allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, - "url-include": {"text": "allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, - "basedir": {"text": "open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "fpm-extensions": {"text": "PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "upload-execution": {"text": "Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks.", "components": ["php8", "laravel"], "sources": ["php8:s2e692b7c86a6", "laravel:s879d69d64852"], "status": "REASONED"}, - "laravel-storage": {"text": "Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution.", "components": ["laravel"], "sources": ["laravel:s879d69d64852"], "status": "REASONED"}, - "upload-size": {"text": "upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "post-size": {"text": "post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "upload-count": {"text": "max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s3cbc65590d52"], "status": "REASONED"}, - "memory-limit": {"text": "memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, - "input-vars": {"text": "max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, - "execution-time": {"text": "max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, - "input-time": {"text": "max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds.", "components": ["php8", "template84"], "sources": ["php8:s5a3f7bde8338", "template84:sd72e3d69551f"], "status": "REASONED"}, - "upload-time": {"text": "Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting.", "components": ["php"], "sources": ["php:s3cbc65590d52"], "status": "REASONED"}, - "fpm-timeout": {"text": "request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "fpm-finished": {"text": "request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, - "session-gc": {"text": "Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes.", "components": ["php8", "php"], "sources": ["php8:s000e155a7b25", "php:s8664dc56a028"], "status": "REASONED"}, - "session-lifetime": {"text": "Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED"}, - "session-only-cookies": {"text": "session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-storage": {"text": "Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, - "session-sid-length": {"text": "PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, - "session-sid-bits": {"text": "PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, - "verify-listener": {"text": "Inventory all FPM listeners: Unix socket or loopback TCP 9000 only.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED", "verify": [1]}, - "verify-tls": {"text": "The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, - "verify-cookies": {"text": "Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED", "verify": [1]}, - "verify-auth": {"text": "Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd"], "status": "REASONED", "verify": [2]}, - "verify-config": {"text": "Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo.", "components": ["php", "php8"], "sources": ["php:s5840a5a0a848", "php8:s89c799077b82", "php8:s2e692b7c86a6"], "status": "REASONED", "verify": [3]}, - "verify-banner": {"text": "Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection.", "components": ["php"], "sources": ["php:s7b2c2c19835d", "php:s2f1c806d037c"], "status": "REASONED", "verify": [4]}, - "verify-error": {"text": "Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested.", "components": ["php8", "php"], "sources": ["php8:s89c799077b82", "php:s2f1c806d037c"], "status": "REASONED", "verify": [5]}, - "verify-laravel-error": {"text": "Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED", "verify": [5]}, - "verify-curl-version": {"text": "The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1, 2, 4, 5]} - } -} ---- # PHP and Laravel: 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): - -- php: PHP documentation (unknown) -- curl: curl manual (7.75.0) -- php8: PHP native configuration (8.x) -- template84: PHP production template (8.4) -- template83: PHP production template (8.3) -- laravel: Laravel (12.x) -- debugbar: Laravel Debugbar (unknown) -- fail2ban: fail2ban (1.1.1) +Written against PHP native configuration 8.x, PHP production template 8.4 and 8.3, Laravel 12.x, fail2ban 1.1.1, and curl manual 7.75.0; 2 further sources carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 65 claims, all reasoned from cited sources. -Claims (full source mapping in the front matter): - -- fpm-private (REASONED; php8): FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM. -- fpm-clients (REASONED; php8): listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP. -- fpm-socket (REASONED; php8): Unix socket access uses listen.owner, listen.group and listen.mode, default 0660. -- cookie-secure (REASONED; php8): Native session.cookie_secure defaults 0; enable it. -- cookie-httponly (REASONED; php8): Native session.cookie_httponly defaults 0; enable it. -- cookie-samesite (REASONED; php8): Native session.cookie_samesite defaults empty; choose Lax or Strict. -- session-strict (REASONED; php8): session.use_strict_mode defaults 0; enable it. -- session-regenerate (REASONED; php): Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion. -- passwords (REASONED; php): PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes. -- plain-auth (REASONED; php, fail2ban): Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here. -- laravel-debug (REASONED; laravel): Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values. -- laravel-key (REASONED; laravel): Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation. -- laravel-secure (REASONED; laravel): Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default. -- laravel-httponly (REASONED; laravel): SESSION_HTTP_ONLY defaults true; retain it. -- laravel-samesite (REASONED; laravel): SESSION_SAME_SITE defaults lax; strict suits admin-only apps. -- laravel-proxy (REASONED; laravel): Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability. -- laravel-urls (REASONED; laravel): URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs. -- laravel-hashes (REASONED; laravel): Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms. -- laravel-limiter (REASONED; laravel): Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route. -- fortify-login (REASONED; laravel): 12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views. -- fortify-mfa (REASONED; laravel): Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes. -- socialite (REASONED; laravel): Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks. -- laravel-cache (REASONED; laravel): Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files. -- laravel-hosts (REASONED; laravel): Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false. -- debug-tools (REASONED; laravel, debugbar): Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration. -- curl-peer (REASONED; php): CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification. -- curl-host (REASONED; php): CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA. -- display-errors (REASONED; php8): PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running. -- startup-errors (REASONED; php8): display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off. -- log-errors (REASONED; php8): log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger. -- error-reporting (REASONED; php8): error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ. -- php-banner (REASONED; php8, php): expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification. -- production-template (REASONED; template84): PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled. -- disable-functions (REASONED; php8): disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary. -- url-fopen (REASONED; php): allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy. -- url-include (REASONED; php): allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen. -- basedir (REASONED; php8): open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components. -- fpm-extensions (REASONED; php8): PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server. -- upload-execution (REASONED; php8, laravel): Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks. -- laravel-storage (REASONED; laravel): Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution. -- upload-size (REASONED; php8): upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead. -- post-size (REASONED; php8): post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app. -- upload-count (REASONED; php8, php): max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit. -- memory-limit (REASONED; php8): memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound. -- input-vars (REASONED; php8): max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit. -- execution-time (REASONED; php8): max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline. -- input-time (REASONED; php8, template84): max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds. -- upload-time (REASONED; php): Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting. -- fpm-timeout (REASONED; php8): request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers. -- fpm-finished (REASONED; php8): request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work. -- session-gc (REASONED; php8, php): Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes. -- session-lifetime (REASONED; php8, laravel): Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives. -- session-only-cookies (REASONED; php8): session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4. -- session-storage (REASONED; php8): Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities. -- session-sid-length (REASONED; php8, template83): PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26. -- session-sid-bits (REASONED; php8, template83): PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5. -- verify-listener (REASONED; php8): Inventory all FPM listeners: Unix socket or loopback TCP 9000 only. -- verify-tls (REASONED; curl): The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration. -- verify-cookies (REASONED; php8, laravel): Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax. -- verify-auth (REASONED; laravel): Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication. -- verify-config (REASONED; php, php8): Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo. -- verify-banner (REASONED; php): Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection. -- verify-error (REASONED; php8, php): Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested. -- verify-laravel-error (REASONED; laravel): Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors. -- verify-curl-version (REASONED; curl): The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual. - +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. PHP normally runs behind a web server (Apache with php-fpm or mod_php, nginx or Caddy with php-fpm), so TLS terminates there: follow [apache.md](apache.md), [nginx.md](nginx.md), or [caddy.md](caddy.md) with a certificate from [free-certificates.md](free-certificates.md). The PHP-specific exposures are the php-fpm FastCGI socket (the PHP manual: "An exposed FastCGI endpoint allows arbitrary code execution"), session cookies that PHP ships without `Secure`, `HttpOnly`, or `SameSite` (all three default off), `APP_DEBUG=true` left on in production, and cURL calls with certificate checks turned off. @@ -586,3 +344,271 @@ REASONED: Laravel exception-route comparison. A live Laravel deployment stand-up - Laravel 12.x file storage (private local disk and public storage link): https://laravel.com/framework/docs/12.x/filesystem - Laravel 12.x Telescope local-only installation: https://laravel.com/framework/docs/12.x/telescope - Laravel Debugbar production-installation guidance: https://github.com/fruitcake/laravel-debugbar + + + +## 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/php.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): + +- php8: PHP native configuration (8.x) +- template84: PHP production template (8.4) +- template83: PHP production template (8.3) +- laravel: Laravel (12.x) +- php: PHP documentation (unknown) +- debugbar: Laravel Debugbar (unknown) +- fail2ban: fail2ban (1.1.1) +- curl: curl manual (7.75.0) + +Claims (full source mapping in the JSON block below): + +- fpm-private (REASONED; php8): FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM. +- fpm-clients (REASONED; php8): listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP. +- fpm-socket (REASONED; php8): Unix socket access uses listen.owner, listen.group and listen.mode, default 0660. +- cookie-secure (REASONED; php8): Native session.cookie_secure defaults 0; enable it. +- cookie-httponly (REASONED; php8): Native session.cookie_httponly defaults 0; enable it. +- cookie-samesite (REASONED; php8): Native session.cookie_samesite defaults empty; choose Lax or Strict. +- session-strict (REASONED; php8): session.use_strict_mode defaults 0; enable it. +- session-regenerate (REASONED; php): Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion. +- passwords (REASONED; php): PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes. +- plain-auth (REASONED; php, fail2ban): Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here. +- laravel-debug (REASONED; laravel): Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values. +- laravel-key (REASONED; laravel): Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation. +- laravel-secure (REASONED; laravel): Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default. +- laravel-httponly (REASONED; laravel): SESSION_HTTP_ONLY defaults true; retain it. +- laravel-samesite (REASONED; laravel): SESSION_SAME_SITE defaults lax; strict suits admin-only apps. +- laravel-proxy (REASONED; laravel): Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability. +- laravel-urls (REASONED; laravel): URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs. +- laravel-hashes (REASONED; laravel): Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms. +- laravel-limiter (REASONED; laravel): Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route. +- fortify-login (REASONED; laravel): 12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views. +- fortify-mfa (REASONED; laravel): Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes. +- socialite (REASONED; laravel): Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks. +- laravel-cache (REASONED; laravel): Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files. +- laravel-hosts (REASONED; laravel): Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false. +- debug-tools (REASONED; laravel, debugbar): Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration. +- curl-peer (REASONED; php): CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification. +- curl-host (REASONED; php): CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA. +- display-errors (REASONED; php8): PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running. +- startup-errors (REASONED; php8): display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off. +- log-errors (REASONED; php8): log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger. +- error-reporting (REASONED; php8): error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ. +- php-banner (REASONED; php8, php): expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification. +- production-template (REASONED; template84): PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled. +- disable-functions (REASONED; php8): disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary. +- url-fopen (REASONED; php): allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy. +- url-include (REASONED; php): allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen. +- basedir (REASONED; php8): open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components. +- fpm-extensions (REASONED; php8): PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server. +- upload-execution (REASONED; php8, laravel): Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks. +- laravel-storage (REASONED; laravel): Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution. +- upload-size (REASONED; php8): upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead. +- post-size (REASONED; php8): post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app. +- upload-count (REASONED; php8, php): max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit. +- memory-limit (REASONED; php8): memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound. +- input-vars (REASONED; php8): max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit. +- execution-time (REASONED; php8): max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline. +- input-time (REASONED; php8, template84): max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds. +- upload-time (REASONED; php): Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting. +- fpm-timeout (REASONED; php8): request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers. +- fpm-finished (REASONED; php8): request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work. +- session-gc (REASONED; php8, php): Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes. +- session-lifetime (REASONED; php8, laravel): Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives. +- session-only-cookies (REASONED; php8): session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4. +- session-storage (REASONED; php8): Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities. +- session-sid-length (REASONED; php8, template83): PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26. +- session-sid-bits (REASONED; php8, template83): PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5. +- verify-listener (REASONED; php8): Inventory all FPM listeners: Unix socket or loopback TCP 9000 only. +- verify-tls (REASONED; curl): The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration. +- verify-cookies (REASONED; php8, laravel): Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax. +- verify-auth (REASONED; laravel): Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication. +- verify-config (REASONED; php, php8): Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo. +- verify-banner (REASONED; php): Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection. +- verify-error (REASONED; php8, php): Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested. +- verify-laravel-error (REASONED; laravel): Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors. +- verify-curl-version (REASONED; curl): The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "b65232e7b90f8edc69a6b0b2639a04b3f7b6b4590b7015c4d1d84cd70d5f9aae", + "components": { + "php8": { + "name": "PHP native configuration", + "basis": "8.x", + "sources": { + "s89c799077b82": "https://www.php.net/manual/en/errorfunc.configuration.php", + "s5a3f7bde8338": "https://www.php.net/manual/en/info.configuration.php", + "sc9e417e2ad6f": "https://www.php.net/manual/en/ini.core.php", + "s2e692b7c86a6": "https://www.php.net/manual/en/install.fpm.configuration.php", + "s000e155a7b25": "https://www.php.net/manual/en/session.configuration.php" + } + }, + "template84": { + "name": "PHP production template", + "basis": "8.4", + "sources": { + "sd72e3d69551f": "https://raw.githubusercontent.com/php/php-src/d313ad6098430f4e61f0121a9e7ab392d195e4e4/php.ini-production" + } + }, + "template83": { + "name": "PHP production template", + "basis": "8.3", + "sources": { + "sea3d3a4ea486": "https://raw.githubusercontent.com/php/php-src/b94f9f68a610e0fca5f2fea5bfa7c7d6d3d5a847/php.ini-production" + } + }, + "laravel": { + "name": "Laravel", + "basis": "12.x", + "sources": { + "sf0448f3d4665": "https://api.laravel.com/docs/12.x/Illuminate/Routing/UrlGenerator.html", + "s98db3429151c": "https://github.com/laravel/laravel/blob/f6b2e79bdbfc5bf4a37ad16466cc06ad79cc9e8f/config/session.php", + "s4e846bd588a8": "https://laravel.com/framework/docs/12.x/configuration", + "s6510c1aa8d96": "https://laravel.com/framework/docs/12.x/deployment", + "sb3e2d08bd07d": "https://laravel.com/framework/docs/12.x/encryption", + "s879d69d64852": "https://laravel.com/framework/docs/12.x/filesystem", + "sc58ba221e3cd": "https://laravel.com/framework/docs/12.x/fortify", + "sdf4ba04ddcda": "https://laravel.com/framework/docs/12.x/hashing", + "s517839d232ec": "https://laravel.com/framework/docs/12.x/requests", + "s248fef722141": "https://laravel.com/framework/docs/12.x/routing", + "s003198079bdd": "https://laravel.com/framework/docs/12.x/socialite", + "sb3e846095c54": "https://laravel.com/framework/docs/12.x/starter-kits", + "sd7d2287b5d35": "https://laravel.com/framework/docs/12.x/telescope" + } + }, + "php": { + "name": "PHP documentation", + "basis": "unknown", + "sources": { + "s09fdbeba0867": "https://www.php.net/manual/en/curl.constants.php", + "s5840a5a0a848": "https://www.php.net/manual/en/features.commandline.options.php", + "s3cbc65590d52": "https://www.php.net/manual/en/features.file-upload.common-pitfalls.php", + "s6f3e55d536e7": "https://www.php.net/manual/en/filesystem.configuration.php", + "s50f85ff4fbd1": "https://www.php.net/manual/en/function.password-hash.php", + "s625cd1d40655": "https://www.php.net/manual/en/function.session-regenerate-id.php", + "s2f1c806d037c": "https://www.php.net/manual/en/function.trigger-error.php", + "s7b2c2c19835d": "https://www.php.net/manual/en/security.hiding.php", + "s8664dc56a028": "https://www.php.net/manual/en/session.security.ini.php" + } + }, + "debugbar": { + "name": "Laravel Debugbar", + "basis": "unknown", + "sources": { + "s7099781fdf4a": "https://github.com/fruitcake/laravel-debugbar" + } + }, + "fail2ban": { + "name": "fail2ban", + "basis": "1.1.1", + "sources": { + "s0470655e109b": "https://github.com/fail2ban/fail2ban/blob/1.1.1/man/jail.conf.5#L187-L200", + "s9b3b926fe020": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/jail.conf#L378-L384", + "sdb5f6f355db6": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/filter.d/nginx-limit-req.conf#L48-L49" + } + }, + "curl": { + "name": "curl manual", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + } + }, + "claims": { + "fpm-private": {"text": "FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-clients": {"text": "listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-socket": {"text": "Unix socket access uses listen.owner, listen.group and listen.mode, default 0660.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "cookie-secure": {"text": "Native session.cookie_secure defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "cookie-httponly": {"text": "Native session.cookie_httponly defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "cookie-samesite": {"text": "Native session.cookie_samesite defaults empty; choose Lax or Strict.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-strict": {"text": "session.use_strict_mode defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-regenerate": {"text": "Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion.", "components": ["php"], "sources": ["php:s625cd1d40655"], "status": "REASONED"}, + "passwords": {"text": "PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes.", "components": ["php"], "sources": ["php:s50f85ff4fbd1"], "status": "REASONED"}, + "plain-auth": {"text": "Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here.", "components": ["php", "fail2ban"], "sources": ["php:s50f85ff4fbd1", "php:s8664dc56a028", "fail2ban:s0470655e109b", "fail2ban:s9b3b926fe020", "fail2ban:sdb5f6f355db6"], "status": "REASONED"}, + "laravel-debug": {"text": "Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, + "laravel-key": {"text": "Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation.", "components": ["laravel"], "sources": ["laravel:sb3e2d08bd07d"], "status": "REASONED"}, + "laravel-secure": {"text": "Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-httponly": {"text": "SESSION_HTTP_ONLY defaults true; retain it.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-samesite": {"text": "SESSION_SAME_SITE defaults lax; strict suits admin-only apps.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-proxy": {"text": "Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, + "laravel-urls": {"text": "URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs.", "components": ["laravel"], "sources": ["laravel:sf0448f3d4665"], "status": "REASONED"}, + "laravel-hashes": {"text": "Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms.", "components": ["laravel"], "sources": ["laravel:sdf4ba04ddcda"], "status": "REASONED"}, + "laravel-limiter": {"text": "Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route.", "components": ["laravel"], "sources": ["laravel:s248fef722141"], "status": "REASONED"}, + "fortify-login": {"text": "12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, + "fortify-mfa": {"text": "Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, + "socialite": {"text": "Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks.", "components": ["laravel"], "sources": ["laravel:s003198079bdd"], "status": "REASONED"}, + "laravel-cache": {"text": "Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, + "laravel-hosts": {"text": "Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, + "debug-tools": {"text": "Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration.", "components": ["laravel", "debugbar"], "sources": ["laravel:sd7d2287b5d35", "debugbar:s7099781fdf4a"], "status": "REASONED"}, + "curl-peer": {"text": "CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, + "curl-host": {"text": "CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, + "display-errors": {"text": "PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "startup-errors": {"text": "display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "log-errors": {"text": "log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "error-reporting": {"text": "error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "php-banner": {"text": "expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s7b2c2c19835d"], "status": "REASONED"}, + "production-template": {"text": "PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled.", "components": ["template84"], "sources": ["template84:sd72e3d69551f"], "status": "REASONED"}, + "disable-functions": {"text": "disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "url-fopen": {"text": "allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, + "url-include": {"text": "allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, + "basedir": {"text": "open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "fpm-extensions": {"text": "PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "upload-execution": {"text": "Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks.", "components": ["php8", "laravel"], "sources": ["php8:s2e692b7c86a6", "laravel:s879d69d64852"], "status": "REASONED"}, + "laravel-storage": {"text": "Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution.", "components": ["laravel"], "sources": ["laravel:s879d69d64852"], "status": "REASONED"}, + "upload-size": {"text": "upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "post-size": {"text": "post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "upload-count": {"text": "max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s3cbc65590d52"], "status": "REASONED"}, + "memory-limit": {"text": "memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "input-vars": {"text": "max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, + "execution-time": {"text": "max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, + "input-time": {"text": "max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds.", "components": ["php8", "template84"], "sources": ["php8:s5a3f7bde8338", "template84:sd72e3d69551f"], "status": "REASONED"}, + "upload-time": {"text": "Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting.", "components": ["php"], "sources": ["php:s3cbc65590d52"], "status": "REASONED"}, + "fpm-timeout": {"text": "request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-finished": {"text": "request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "session-gc": {"text": "Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes.", "components": ["php8", "php"], "sources": ["php8:s000e155a7b25", "php:s8664dc56a028"], "status": "REASONED"}, + "session-lifetime": {"text": "Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED"}, + "session-only-cookies": {"text": "session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-storage": {"text": "Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-sid-length": {"text": "PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, + "session-sid-bits": {"text": "PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, + "verify-listener": {"text": "Inventory all FPM listeners: Unix socket or loopback TCP 9000 only.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-cookies": {"text": "Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd"], "status": "REASONED", "verify": [2]}, + "verify-config": {"text": "Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo.", "components": ["php", "php8"], "sources": ["php:s5840a5a0a848", "php8:s89c799077b82", "php8:s2e692b7c86a6"], "status": "REASONED", "verify": [3]}, + "verify-banner": {"text": "Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection.", "components": ["php"], "sources": ["php:s7b2c2c19835d", "php:s2f1c806d037c"], "status": "REASONED", "verify": [4]}, + "verify-error": {"text": "Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested.", "components": ["php8", "php"], "sources": ["php8:s89c799077b82", "php:s2f1c806d037c"], "status": "REASONED", "verify": [5]}, + "verify-laravel-error": {"text": "Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED", "verify": [5]}, + "verify-curl-version": {"text": "The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1, 2, 4, 5]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/python.md b/plugin/skills/secure-deployment-config/references/python.md index 8d872c74e..09f7ce969 100644 --- a/plugin/skills/secure-deployment-config/references/python.md +++ b/plugin/skills/secure-deployment-config/references/python.md @@ -1,10 +1,268 @@ ---- -version_basis: { +# Python web apps: TLS and authentication + +Written against Django documentation 6.1, Python http.server documentation 3.14.4, Python http.server source v3.14.4, Python standard library documentation 3.14, CPython ssl source and documentation v3.14.8, CPython secrets documentation and source v3.14.8, PyOTP v2.9.0, and qrcode v8.2, plus 6 more recorded bases; 5 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 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. + +Covers Flask, FastAPI/Uvicorn, Gunicorn, and Django. This guide's preferred production layout: bind the app server to `127.0.0.1` and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). For managed HTTP ingress, bind the listener required by your deployment; TLS termination depends on the platform and configuration described below ([paas.md](paas.md)). The app servers can also terminate TLS themselves, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). + +## 1. TLS per server + +Flask's built-in server (development only; it is not a production server, TLS or not): + +```python +app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem")) +# ssl_context="adhoc" generates a throwaway self-signed cert; requires the cryptography package +``` + +Gunicorn (Flask/Django/WSGI in production): + +```bash +gunicorn --bind 0.0.0.0:8443 \ + --certfile /etc/ssl/certs/server.crt \ + --keyfile /etc/ssl/private/server.key \ + app:app +``` + +Uvicorn (FastAPI/ASGI): + +```bash +uvicorn main:app --host 0.0.0.0 --port 8443 \ + --ssl-certfile /etc/ssl/certs/server.crt \ + --ssl-keyfile /etc/ssl/private/server.key +``` + +This guide's advice: Bind to `0.0.0.0` only when either (a) the process itself terminates TLS with authentication in place, or (b) a platform terminates TLS at its ingress and reaches your app over a private or container network. Otherwise, on a host you expose directly, keep `127.0.0.1` and terminate TLS in a reverse proxy. + +This guide's advice for managed platforms: do not add extra listeners; bind only the listener required by your deployment. The documented HTTP ingress mappings are: + +- Render web services must bind `0.0.0.0`. Render recommends the `PORT` environment variable (at the time of writing, default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound ([web services](https://render.com/docs/web-services)). +- Heroku assigns each web process its port through `PORT` ([runtime principles](https://devcenter.heroku.com/articles/runtime-principles)). Fir Private Spaces networking uses IPv6 by default; Heroku warns that IPv4-only assumptions do not bind properly and recommends an IPv6 or dual-stack listener, typically `::` ([generations](https://devcenter.heroku.com/articles/generations)). These pages do not establish Cedar's listener address. +- Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the configured `internal_port` to the app listener ([service binding](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42)). IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime ([wildcard qualification](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92)). +- Railway domains route to a detected or selected internal target port; the cited passage does not establish a required listener address, an injected `PORT` or the TLS termination location ([target ports](https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146)). + +Render's load balancer terminates inbound HTTPS and forwards HTTP to the web service ([web services](https://render.com/docs/web-services)). Heroku's HTTP routing page describes a load balancer offering SSL termination and routers forwarding HTTP to web dynos; it notes that both Private Spaces generations have different routers, so this is not a complete account of Private Spaces routing ([HTTP routing](https://devcenter.heroku.com/articles/http-routing)). Fly Proxy's `tls` handler terminates TLS and forwards plaintext ([TLS handler](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176)); without handlers it passes TCP through, allowing the app to terminate TLS itself ([TCP passthrough](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97)). This guide's advice: plain HTTP is acceptable only behind protected ingress that terminates TLS; application authentication is still required ([paas.md](paas.md)). + +## 2. Django settings for HTTPS + +```python +SECURE_SSL_REDIRECT = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # only behind a proxy that STRIPS the client's value and sets its own +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_HSTS_SECONDS = 3600 # start small; raise to 31536000 (1 year) after HTTPS is confirmed working +SECURE_HSTS_INCLUDE_SUBDOMAINS = False # True ONLY after every subdomain also serves valid HTTPS; the commitment is irreversible for SECURE_HSTS_SECONDS +``` + +`SECURE_PROXY_SSL_HEADER` must be set only when a proxy you control, or a managed platform that reliably guarantees this, strips any client-supplied `X-Forwarded-Proto` from every request and sets it itself from the real connection scheme; otherwise a client can spoof it and make Django treat plain HTTP as HTTPS. Leave it unset if you are not behind such a proxy, and confirm the app is reachable only through that proxy, not directly. Run `python manage.py check --deploy` and fix what it reports. + +## 3. Authentication + +Follow [authentication.md](authentication.md). Framework specifics: + +- Keep Django's built-in password hashing; do not replace it with custom code (this guide's advice). +- If your Flask or FastAPI application implements its own password accounts, provide its account storage and hash passwords with `argon2-cffi` or `bcrypt` (this guide's advice; follow the algorithm and legacy-bcrypt constraints in [authentication.md](authentication.md)): + +```python +from argon2 import PasswordHasher +ph = PasswordHasher() +hash_ = ph.hash(password) +ph.verify(hash_, password) # raises on mismatch +``` + +The [argon2-cffi 25.1.0 example](https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28) documents these `PasswordHasher`, `hash` and `verify` calls and a `VerifyMismatchError` on mismatch. The [pinned bcrypt example](https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76) documents `hashpw(password, gensalt())` and `checkpw(password, hashed)`; these API examples do not establish framework user-store capabilities. + +- Generate tokens and secrets with the standard library, and load them from the environment (this guide's advice): + +```python +import secrets +token = secrets.token_urlsafe(32) +``` + +In [CPython v3.14.8](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103), `token_urlsafe(nbytes=None)` returns URL-safe Base64 text from `nbytes` random bytes; the argument counts input bytes, not output characters. The explicit `32` above is this guide's example choice, not a size suitable for every secret. If the argument is omitted or `None`, it uses `DEFAULT_ENTROPY`, [32 bytes at this pin](https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45); the [documented default can change](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130), including in maintenance releases. + +- FastAPI's `fastapi.security` classes (`HTTPBearer`, `APIKeyHeader`, `OAuth2AuthorizationCodeBearer`, and so on) extract the credential from the request and declare the OpenAPI security scheme; what they check varies by helper (an API-key helper checks only that a value is present; the HTTP and OAuth2 bearer helpers may also check the scheme), but none authenticate the credential (no signature, issuer, audience, or expiry check), and `OpenIdConnect` is documented as a stub that does not implement the scheme or use the discovery URL. Use them to extract the token, then validate it (signature, issuer, audience, expiry) with an OIDC library such as Authlib, and authorize per [oidc-integration.md](oidc-integration.md). +- Rate-limit login routes (this guide's advice; for example with a proxy-level limit or a library such as slowapi for Starlette/FastAPI ASGI apps' HTTP endpoints; endpoints using slowapi's `limit` decorator must take an explicit `request` argument, and slowapi does not support WebSocket endpoints). +- MFA: add TOTP with [pyotp](https://github.com/pyauth/pyotp) plus the [qrcode](https://pypi.org/project/qrcode/) package for enrolment QR codes; [django-otp](https://pypi.org/project/django-otp/) integrates this into Django. Requirements and options in [mfa.md](mfa.md). + +## 4. Client-side TLS discipline + +Never ship `verify=False` (requests/httpx) or `ssl._create_unverified_context` (this guide's advice). For an internal CA, point the client at it instead (this guide's advice): + +```bash +export REQUESTS_CA_BUNDLE=/path/ca.crt # requests +export SSL_CERT_FILE=/path/ca.crt # HTTPX with verify=True and trust_env=True; OpenSSL default CA file for ssl when its default verify paths apply (see caveat below) +``` + +HTTPX 0.28.1's SSL guide says `SSL_CERT_FILE` and `SSL_CERT_DIR` are not automatically used and shows an explicit context; its code path contradicts that: its sync and async transports pass `verify` and `trust_env` to `create_ssl_context`, which reads `SSL_CERT_FILE` when both are true. Its environment guide says environment variables are used by default but does not list `SSL_CERT_FILE`. This guide's advice: use the export above for that HTTPX code path; pass an explicit CA context for other configurations. With no CA specified and verification enabled, CPython's `ssl.create_default_context()` tries to load default CAs (CPython notes this may fail silently). `ssl.get_default_verify_paths()` reports OpenSSL's environment key for a CA file; OpenSSL names `SSL_CERT_FILE` as an override for its default CA file. CPython's cited files do not name `SSL_CERT_FILE`, so whether a given `ssl` configuration uses that override remains unverified. + +## 5. Do not quick-share files with `http.server` + +`python -m http.server` is a common quick-share suggestion, and it is the wrong one on any reachable host. By default it binds every interface (as of Python 3.14.4), and it serves the current directory: source, `.env`, private keys, and database dumps are all downloadable by anyone who can reach the port. It also follows symbolic links, so a link in that directory hands out files from outside it, and the Python docs mark the module as not suitable for production. If you have no alternative, bind loopback and serve a directory that holds only what you mean to share: + +```bash +cd /path/to/a/directory/with/nothing/private || exit +python -m http.server -b 127.0.0.1 8000 +``` + +Reach it through an SSH tunnel, including one that runs over your tailnet, never a public bind. + +## 6. Verify (REASONED: TLS, authentication and listener expectations follow the Sources below and linked deployment guidance; no deployment run or outcome is recorded in this guide. This metadata-only review has no deployed Python application, proxy or managed ingress to test.) + +```bash +curl -q -sSI --noproxy '*' https://example.com/ # public HTTPS endpoint: must succeed without -k; if not, investigate the cert, chain, hostname, or your client trust store, never -k +# An unauthenticated request to a real protected path must be REJECTED and a valid credential ACCEPTED. --noproxy +# disables YOUR client proxy, not a reverse proxy in front of the app, so if the app sits behind an auth proxy this +# pair cannot tell whether the app or the proxy enforced it: confirm the layer by probing the app directly or in its logs. +curl -q -g -sS --noproxy '*' -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # no credentials: expect 401/403 +curl -q -g -sS --noproxy '*' -H @cred.txt -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # valid credential from a file (cred.txt holds a full header line, e.g. "Authorization: Bearer ..."): expect 2xx +ss -tlnp # every listener: loopback for a same-host reverse proxy, or the platform's required address and port from section 1 on managed ingress +``` + +## Sources (checked September 2026) + +- Python `http.server` (binds all interfaces by default, serves the current directory, follows symlinks, not for production) (Python 3.14.4): https://docs.python.org/3/library/http.server.html +- Gunicorn documentation (settings reference: bind, certfile, keyfile, ca_certs): https://gunicorn.org/reference/settings/ +- Uvicorn settings reference: https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md +- Django deployment checklist: https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ +- argon2-cffi: https://argon2-cffi.readthedocs.io/ +- argon2-cffi PasswordHasher, hash and verify mismatch example (25.1.0, checked October 2026): https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28 +- Python bcrypt hashpw, gensalt and checkpw example (commit 2b4ba9ac84df972e8e81311d09559af8dc82ef33, checked October 2026): https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76 +- Werkzeug serving (`ssl_context="adhoc"` requires cryptography): https://werkzeug.palletsprojects.com/en/stable/serving/ +- FastAPI security reference: https://fastapi.tiangolo.com/reference/security/ ; `OpenIdConnect` source (stub warning): https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py +- `http.server` binds with `bind=None` and `AI_PASSIVE`, the wildcard address (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334 +- `python -m http.server`'s `-b/--bind` has no default value, and its help says "default: all interfaces" (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373 +- Python 3.14 secrets.token_urlsafe (series documentation, checked October 2026): https://docs.python.org/3.14/library/secrets.html +- CPython token_urlsafe input bytes and Base64 output (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103 +- CPython secrets default entropy and token_bytes fallback (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45 +- CPython secrets default may change in maintenance releases (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130 +- Python 3.14 os.getenv environment loading (series documentation, checked October 2026): https://docs.python.org/3.14/library/os.html#os.getenv +- PyOTP one-time passwords and MFA (v2.9.0, checked October 2026): https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5 +- qrcode enrolment image generation (v8.2, checked October 2026): https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44 +- django-otp Django authentication integration (1.6.1, checked October 2026): https://pypi.org/project/django-otp/1.6.1/ +- Requests REQUESTS_CA_BUNDLE trust roots (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 +- slowapi Starlette/FastAPI scope (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3 +- slowapi limit decorator, endpoint request argument and WebSocket limitation (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50 +- HTTPX environment-variable default and trust_env (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7 +- HTTPX environment guide variable list, which omits SSL_CERT_FILE (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53 +- HTTPX SSL guide, verify=False example (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17 +- HTTPX SSL guide, conflicting SSL_CERT_FILE statement (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86 +- HTTPX SSL context selection with verify=True and trust_env=True (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40 +- HTTPX sync transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154 +- HTTPX async transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298 +- CPython ssl get_default_verify_paths (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405 +- CPython ssl load_default_certs (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548 +- CPython ssl create_default_context conditional default-CA load (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736 +- CPython ssl _create_unverified_context (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754 +- CPython ssl documentation for default verify paths and OpenSSL environment keys (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433 +- CPython ssl documentation for default CAs and silent failure (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649 +- OpenSSL default CA file environment override (openssl-4.0.3, checked October 2026): https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49 +- Requests verify=False disables certificate verification (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243 +- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services +- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing +- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles +- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations +- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 +- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 +- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 +- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 +- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 + + + +## 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/python.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): + +- django: Django documentation (6.1) +- python: Python http.server documentation (3.14.4) +- python-pin: Python http.server source (v3.14.4) +- stdlib-docs: Python standard library documentation (3.14) +- cpython-ssl: CPython ssl source and documentation (v3.14.8) +- cpython-secrets: CPython secrets documentation and source (v3.14.8) +- gunicorn: Gunicorn documentation (unknown) +- uvicorn: Uvicorn source (5ac6265a01ff6dcadb0e4250152c3deaf8a168a9) +- fastapi: FastAPI documentation (unknown) +- oidc-stub: FastAPI OpenIdConnect source (31bbb380748ccead62fc0f42dbf4273f11dadccf) +- bcrypt-api: Python bcrypt API example (2b4ba9ac84df972e8e81311d09559af8dc82ef33) +- werkzeug: Werkzeug documentation (unknown) +- argon: argon2-cffi documentation (unknown) +- pyotp: PyOTP (v2.9.0) +- qrcode: qrcode (v8.2) +- django-otp: django-otp (1.6.1) +- requests: Requests (v2.32.3) +- slowapi: slowapi source (v0.1.10) +- httpx: HTTPX source and documentation (0.28.1) +- openssl: OpenSSL documentation (openssl-4.0.3) +- argon-api: argon2-cffi API example (25.1.0) +- render: Render documentation (unknown) +- heroku: Heroku documentation (unknown) +- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) +- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) + +Claims (full source mapping in the JSON block below): + +- bind (REASONED; gunicorn, uvicorn, render, heroku, fly-pinned, railway): This guide advises loopback for same-host TLS proxies, no extra managed listeners, and wildcard only for authenticated direct TLS or private TLS ingress; app auth remains required. Rules vary (Fir Private Spaces: IPv6/dual-stack recommended; Fly without handlers: TCP passthrough); Railway address/injected port/TLS location and Cedar address remain uncited. +- flask-tls (REASONED; werkzeug): Flask development server binds 127.0.0.1:8443 with cert/key ssl_context; adhoc requires cryptography and remains development-only. +- gunicorn-tls (REASONED; gunicorn): Gunicorn binds 0.0.0.0:8443 with --certfile and --keyfile for direct TLS. +- uvicorn-tls (REASONED; uvicorn): Uvicorn binds 0.0.0.0:8443 with --ssl-certfile and --ssl-keyfile for direct TLS. +- django-redirect (REASONED; django): Set SECURE_SSL_REDIRECT=True. +- django-proxy (REASONED; django): Set SECURE_PROXY_SSL_HEADER only behind a trusted proxy that strips and replaces X-Forwarded-Proto; prevent direct bypass. +- django-cookies (REASONED; django): Set SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE true. +- django-hsts (REASONED; django): Begin HSTS at 3600 seconds and include-subdomains false; raise duration and scope only after valid HTTPS is confirmed. +- deploy-check (REASONED; django): Run manage.py check --deploy and fix its findings. +- password (REASONED; django, argon, argon-api, bcrypt-api): Keep Django's built-in hashing; do not replace it with custom code (guide advice). For Flask/FastAPI apps implementing local password accounts, this guide advises account storage and argon2-cffi or bcrypt under authentication.md's constraints; argon2-cffi 25.1.0's PasswordHasher.verify raises VerifyMismatchError on mismatch, and these APIs do not establish framework user-store capabilities. +- secrets (REASONED; django, stdlib-docs, cpython-secrets): Generate secrets.token_urlsafe(32) and load secrets from the environment (guide advice); 32 counts random input bytes and is this guide's example choice, not a universal size. CPython v3.14.8 uses DEFAULT_ENTROPY (32 bytes) when omitted or None, but that default may change, including in maintenance releases. +- credential-extraction (REASONED; fastapi): FastAPI security helpers extract credentials and declare schemes; presence/scheme checks do not verify signature, issuer, audience or expiry. +- oidc (REASONED; fastapi, oidc-stub): OpenIdConnect is a stub and does not use its discovery URL; validate extracted tokens with an OIDC library and authorize separately. +- login-limit (REASONED; fastapi, slowapi): Rate-limiting login routes and a proxy-level limit are this guide's advice. slowapi v0.1.10 documents limits for Starlette/FastAPI HTTP endpoints; endpoints using its limit decorator must take an explicit request argument, and WebSocket endpoints are not supported. +- mfa (REASONED; django, pyotp, qrcode, django-otp): Use pyotp and qrcode, django-otp for Django, or linked MFA guidance. +- client-validation (REASONED; python, requests, httpx, cpython-ssl): Never ship requests/httpx verify=False or ssl._create_unverified_context; HTTPX documents verify=False and CPython defines _create_unverified_context. The prohibition is this guide's advice. +- client-ca (REASONED; python, requests, httpx, cpython-ssl, openssl): REQUESTS_CA_BUNDLE selects a requests CA. HTTPX 0.28.1's transport passes verify and trust_env to create_ssl_context, which reads SSL_CERT_FILE when verify=True and trust_env=True; its SSL guide contradicts that source. Its environment guide says environment variables are used by default but does not list SSL_CERT_FILE. CPython's ssl module reports OpenSSL default verify paths; with no CA specified and verification enabled, ssl.create_default_context() tries to load default CAs, which may fail silently. OpenSSL names SSL_CERT_FILE as an environment override for its default CA file; CPython's cited files do not name that variable, so whether a given ssl configuration uses it remains unverified. The export and explicit-context choices are this guide's advice. +- http-server-bind (REASONED; python-pin): As of Python 3.14.4, http.server binds every interface by default; explicitly bind 127.0.0.1:8000. +- http-server-files (REASONED; python): http.server serves the current directory and follows symlinks outside it; it is unsuitable for production. Share only a non-private directory through an SSH tunnel. +- verify-tls (REASONED; gunicorn, uvicorn): Public HTTPS must succeed without -k; certificate, chain, hostname and trust failures need investigation. +- verify-auth (REASONED; django, fastapi): A real protected route denies no credentials with 401/403 and accepts valid file-supplied credentials with 2xx; logs or direct probing distinguish app from proxy enforcement. +- verify-bind (REASONED; gunicorn, uvicorn): Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port. + +````json +{ "schema": 1, "checked": "2026-10-18", "documentation_checked": "2026-09", "body_sha256": "325223859acb920caba7e11658d3c3b87a9f7703faeab9e05b7641db8696fd48", "components": { + "django": { + "name": "Django documentation", + "basis": "6.1", + "sources": { + "s7289e1c111ae": "https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/" + } + }, "python": { "name": "Python http.server documentation", "basis": "3.14.4", @@ -12,39 +270,55 @@ version_basis: { "s8fae94406709": "https://docs.python.org/3/library/http.server.html" } }, - "gunicorn": { - "name": "Gunicorn documentation", - "basis": "unknown", + "python-pin": { + "name": "Python http.server source", + "basis": "v3.14.4", "sources": { - "s72daf362300c": "https://gunicorn.org/reference/settings/" + "s0038729960a0": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334", + "sa3111578d65d": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373" } }, - "uvicorn": { - "name": "Uvicorn source", - "basis": "5ac6265a01ff6dcadb0e4250152c3deaf8a168a9", + "stdlib-docs": { + "name": "Python standard library documentation", + "basis": "3.14", "sources": { - "s02429a4a09d9": "https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md" + "s971d128efc65": "https://docs.python.org/3.14/library/secrets.html", + "sdad376a711c3": "https://docs.python.org/3.14/library/os.html#os.getenv" } }, - "django": { - "name": "Django documentation", - "basis": "6.1", + "cpython-ssl": { + "name": "CPython ssl source and documentation", + "basis": "v3.14.8", "sources": { - "s7289e1c111ae": "https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/" + "sbac049853cda": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405", + "s6dcc06feaaaf": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548", + "s4d7f143c9456": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736", + "s4a1cd173d596": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754", + "s79d6191e12eb": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433", + "s82c72b9a971a": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649" } }, - "argon": { - "name": "argon2-cffi documentation", - "basis": "unknown", + "cpython-secrets": { + "name": "CPython secrets documentation and source", + "basis": "v3.14.8", "sources": { - "sff2b9a487e67": "https://argon2-cffi.readthedocs.io/" + "s31e1a792a6b1": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103", + "s7cc14a5ebfa0": "https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45", + "sdef0e1600064": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130" } }, - "werkzeug": { - "name": "Werkzeug documentation", + "gunicorn": { + "name": "Gunicorn documentation", "basis": "unknown", "sources": { - "sd5066c63a3ca": "https://werkzeug.palletsprojects.com/en/stable/serving/" + "s72daf362300c": "https://gunicorn.org/reference/settings/" + } + }, + "uvicorn": { + "name": "Uvicorn source", + "basis": "5ac6265a01ff6dcadb0e4250152c3deaf8a168a9", + "sources": { + "s02429a4a09d9": "https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md" } }, "fastapi": { @@ -61,20 +335,25 @@ version_basis: { "s876e3d51e7ac": "https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py" } }, - "python-pin": { - "name": "Python http.server source", - "basis": "v3.14.4", + "bcrypt-api": { + "name": "Python bcrypt API example", + "basis": "2b4ba9ac84df972e8e81311d09559af8dc82ef33", "sources": { - "s0038729960a0": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334", - "sa3111578d65d": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373" + "s340a2923b6da": "https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76" } }, - "stdlib-docs": { - "name": "Python standard library documentation", - "basis": "3.14", + "werkzeug": { + "name": "Werkzeug documentation", + "basis": "unknown", "sources": { - "s971d128efc65": "https://docs.python.org/3.14/library/secrets.html", - "sdad376a711c3": "https://docs.python.org/3.14/library/os.html#os.getenv" + "sd5066c63a3ca": "https://werkzeug.palletsprojects.com/en/stable/serving/" + } + }, + "argon": { + "name": "argon2-cffi documentation", + "basis": "unknown", + "sources": { + "sff2b9a487e67": "https://argon2-cffi.readthedocs.io/" } }, "pyotp": { @@ -121,22 +400,10 @@ version_basis: { "s52608d6c5e47": "https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7", "sd33b79237122": "https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53", "s928d85a8d13e": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17", - "s3734fbccdf40": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86", - "s30dd14bf172e": "https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40", - "s4a1f3b43f184": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154", - "sc5a61a9b2f64": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298" - } - }, - "cpython-ssl": { - "name": "CPython ssl source and documentation", - "basis": "v3.14.8", - "sources": { - "sbac049853cda": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405", - "s6dcc06feaaaf": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548", - "s4d7f143c9456": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736", - "s4a1cd173d596": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754", - "s79d6191e12eb": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433", - "s82c72b9a971a": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649" + "s3734fbccdf40": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86", + "s30dd14bf172e": "https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40", + "s4a1f3b43f184": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154", + "sc5a61a9b2f64": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298" } }, "openssl": { @@ -146,6 +413,13 @@ version_basis: { "s592eb65718de": "https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49" } }, + "argon-api": { + "name": "argon2-cffi API example", + "basis": "25.1.0", + "sources": { + "s193f86db0a79": "https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28" + } + }, "render": { "name": "Render documentation", "basis": "unknown", @@ -178,29 +452,6 @@ version_basis: { "sources": { "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146" } - }, - "argon-api": { - "name": "argon2-cffi API example", - "basis": "25.1.0", - "sources": { - "s193f86db0a79": "https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28" - } - }, - "bcrypt-api": { - "name": "Python bcrypt API example", - "basis": "2b4ba9ac84df972e8e81311d09559af8dc82ef33", - "sources": { - "s340a2923b6da": "https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76" - } - }, - "cpython-secrets": { - "name": "CPython secrets documentation and source", - "basis": "v3.14.8", - "sources": { - "s31e1a792a6b1": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103", - "s7cc14a5ebfa0": "https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45", - "sdef0e1600064": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130" - } } }, "claims": { @@ -228,230 +479,5 @@ version_basis: { "verify-bind": {"text": "Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port.", "components": ["gunicorn", "uvicorn"], "sources": ["gunicorn:s72daf362300c", "uvicorn:s02429a4a09d9"], "status": "REASONED", "verify": [1]} } } ---- -# Python web apps: 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): - -- python: Python http.server documentation (3.14.4) -- gunicorn: Gunicorn documentation (unknown) -- uvicorn: Uvicorn source (5ac6265a01ff6dcadb0e4250152c3deaf8a168a9) -- django: Django documentation (6.1) -- argon: argon2-cffi documentation (unknown) -- werkzeug: Werkzeug documentation (unknown) -- fastapi: FastAPI documentation (unknown) -- oidc-stub: FastAPI OpenIdConnect source (31bbb380748ccead62fc0f42dbf4273f11dadccf) -- python-pin: Python http.server source (v3.14.4) -- stdlib-docs: Python standard library documentation (3.14) -- pyotp: PyOTP (v2.9.0) -- qrcode: qrcode (v8.2) -- django-otp: django-otp (1.6.1) -- requests: Requests (v2.32.3) -- slowapi: slowapi source (v0.1.10) -- httpx: HTTPX source and documentation (0.28.1) -- cpython-ssl: CPython ssl source and documentation (v3.14.8) -- openssl: OpenSSL documentation (openssl-4.0.3) -- render: Render documentation (unknown) -- heroku: Heroku documentation (unknown) -- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) -- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) -- argon-api: argon2-cffi API example (25.1.0) -- bcrypt-api: Python bcrypt API example (2b4ba9ac84df972e8e81311d09559af8dc82ef33) -- cpython-secrets: CPython secrets documentation and source (v3.14.8) - -Claims (full source mapping in the front matter): - -- bind (REASONED; gunicorn, uvicorn, render, heroku, fly-pinned, railway): This guide advises loopback for same-host TLS proxies, no extra managed listeners, and wildcard only for authenticated direct TLS or private TLS ingress; app auth remains required. Rules vary (Fir Private Spaces: IPv6/dual-stack recommended; Fly without handlers: TCP passthrough); Railway address/injected port/TLS location and Cedar address remain uncited. -- flask-tls (REASONED; werkzeug): Flask development server binds 127.0.0.1:8443 with cert/key ssl_context; adhoc requires cryptography and remains development-only. -- gunicorn-tls (REASONED; gunicorn): Gunicorn binds 0.0.0.0:8443 with --certfile and --keyfile for direct TLS. -- uvicorn-tls (REASONED; uvicorn): Uvicorn binds 0.0.0.0:8443 with --ssl-certfile and --ssl-keyfile for direct TLS. -- django-redirect (REASONED; django): Set SECURE_SSL_REDIRECT=True. -- django-proxy (REASONED; django): Set SECURE_PROXY_SSL_HEADER only behind a trusted proxy that strips and replaces X-Forwarded-Proto; prevent direct bypass. -- django-cookies (REASONED; django): Set SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE true. -- django-hsts (REASONED; django): Begin HSTS at 3600 seconds and include-subdomains false; raise duration and scope only after valid HTTPS is confirmed. -- deploy-check (REASONED; django): Run manage.py check --deploy and fix its findings. -- password (REASONED; django, argon, argon-api, bcrypt-api): Keep Django's built-in hashing; do not replace it with custom code (guide advice). For Flask/FastAPI apps implementing local password accounts, this guide advises account storage and argon2-cffi or bcrypt under authentication.md's constraints; argon2-cffi 25.1.0's PasswordHasher.verify raises VerifyMismatchError on mismatch, and these APIs do not establish framework user-store capabilities. -- secrets (REASONED; django, stdlib-docs, cpython-secrets): Generate secrets.token_urlsafe(32) and load secrets from the environment (guide advice); 32 counts random input bytes and is this guide's example choice, not a universal size. CPython v3.14.8 uses DEFAULT_ENTROPY (32 bytes) when omitted or None, but that default may change, including in maintenance releases. -- credential-extraction (REASONED; fastapi): FastAPI security helpers extract credentials and declare schemes; presence/scheme checks do not verify signature, issuer, audience or expiry. -- oidc (REASONED; fastapi, oidc-stub): OpenIdConnect is a stub and does not use its discovery URL; validate extracted tokens with an OIDC library and authorize separately. -- login-limit (REASONED; fastapi, slowapi): Rate-limiting login routes and a proxy-level limit are this guide's advice. slowapi v0.1.10 documents limits for Starlette/FastAPI HTTP endpoints; endpoints using its limit decorator must take an explicit request argument, and WebSocket endpoints are not supported. -- mfa (REASONED; django, pyotp, qrcode, django-otp): Use pyotp and qrcode, django-otp for Django, or linked MFA guidance. -- client-validation (REASONED; python, requests, httpx, cpython-ssl): Never ship requests/httpx verify=False or ssl._create_unverified_context; HTTPX documents verify=False and CPython defines _create_unverified_context. The prohibition is this guide's advice. -- client-ca (REASONED; python, requests, httpx, cpython-ssl, openssl): REQUESTS_CA_BUNDLE selects a requests CA. HTTPX 0.28.1's transport passes verify and trust_env to create_ssl_context, which reads SSL_CERT_FILE when verify=True and trust_env=True; its SSL guide contradicts that source. Its environment guide says environment variables are used by default but does not list SSL_CERT_FILE. CPython's ssl module reports OpenSSL default verify paths; with no CA specified and verification enabled, ssl.create_default_context() tries to load default CAs, which may fail silently. OpenSSL names SSL_CERT_FILE as an environment override for its default CA file; CPython's cited files do not name that variable, so whether a given ssl configuration uses it remains unverified. The export and explicit-context choices are this guide's advice. -- http-server-bind (REASONED; python-pin): As of Python 3.14.4, http.server binds every interface by default; explicitly bind 127.0.0.1:8000. -- http-server-files (REASONED; python): http.server serves the current directory and follows symlinks outside it; it is unsuitable for production. Share only a non-private directory through an SSH tunnel. -- verify-tls (REASONED; gunicorn, uvicorn): Public HTTPS must succeed without -k; certificate, chain, hostname and trust failures need investigation. -- verify-auth (REASONED; django, fastapi): A real protected route denies no credentials with 401/403 and accepts valid file-supplied credentials with 2xx; logs or direct probing distinguish app from proxy enforcement. -- verify-bind (REASONED; gunicorn, uvicorn): Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port. +```` - -Covers Flask, FastAPI/Uvicorn, Gunicorn, and Django. This guide's preferred production layout: bind the app server to `127.0.0.1` and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). For managed HTTP ingress, bind the listener required by your deployment; TLS termination depends on the platform and configuration described below ([paas.md](paas.md)). The app servers can also terminate TLS themselves, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). - -## 1. TLS per server - -Flask's built-in server (development only; it is not a production server, TLS or not): - -```python -app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem")) -# ssl_context="adhoc" generates a throwaway self-signed cert; requires the cryptography package -``` - -Gunicorn (Flask/Django/WSGI in production): - -```bash -gunicorn --bind 0.0.0.0:8443 \ - --certfile /etc/ssl/certs/server.crt \ - --keyfile /etc/ssl/private/server.key \ - app:app -``` - -Uvicorn (FastAPI/ASGI): - -```bash -uvicorn main:app --host 0.0.0.0 --port 8443 \ - --ssl-certfile /etc/ssl/certs/server.crt \ - --ssl-keyfile /etc/ssl/private/server.key -``` - -This guide's advice: Bind to `0.0.0.0` only when either (a) the process itself terminates TLS with authentication in place, or (b) a platform terminates TLS at its ingress and reaches your app over a private or container network. Otherwise, on a host you expose directly, keep `127.0.0.1` and terminate TLS in a reverse proxy. - -This guide's advice for managed platforms: do not add extra listeners; bind only the listener required by your deployment. The documented HTTP ingress mappings are: - -- Render web services must bind `0.0.0.0`. Render recommends the `PORT` environment variable (at the time of writing, default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound ([web services](https://render.com/docs/web-services)). -- Heroku assigns each web process its port through `PORT` ([runtime principles](https://devcenter.heroku.com/articles/runtime-principles)). Fir Private Spaces networking uses IPv6 by default; Heroku warns that IPv4-only assumptions do not bind properly and recommends an IPv6 or dual-stack listener, typically `::` ([generations](https://devcenter.heroku.com/articles/generations)). These pages do not establish Cedar's listener address. -- Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the configured `internal_port` to the app listener ([service binding](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42)). IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime ([wildcard qualification](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92)). -- Railway domains route to a detected or selected internal target port; the cited passage does not establish a required listener address, an injected `PORT` or the TLS termination location ([target ports](https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146)). - -Render's load balancer terminates inbound HTTPS and forwards HTTP to the web service ([web services](https://render.com/docs/web-services)). Heroku's HTTP routing page describes a load balancer offering SSL termination and routers forwarding HTTP to web dynos; it notes that both Private Spaces generations have different routers, so this is not a complete account of Private Spaces routing ([HTTP routing](https://devcenter.heroku.com/articles/http-routing)). Fly Proxy's `tls` handler terminates TLS and forwards plaintext ([TLS handler](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176)); without handlers it passes TCP through, allowing the app to terminate TLS itself ([TCP passthrough](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97)). This guide's advice: plain HTTP is acceptable only behind protected ingress that terminates TLS; application authentication is still required ([paas.md](paas.md)). - -## 2. Django settings for HTTPS - -```python -SECURE_SSL_REDIRECT = True -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # only behind a proxy that STRIPS the client's value and sets its own -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -SECURE_HSTS_SECONDS = 3600 # start small; raise to 31536000 (1 year) after HTTPS is confirmed working -SECURE_HSTS_INCLUDE_SUBDOMAINS = False # True ONLY after every subdomain also serves valid HTTPS; the commitment is irreversible for SECURE_HSTS_SECONDS -``` - -`SECURE_PROXY_SSL_HEADER` must be set only when a proxy you control, or a managed platform that reliably guarantees this, strips any client-supplied `X-Forwarded-Proto` from every request and sets it itself from the real connection scheme; otherwise a client can spoof it and make Django treat plain HTTP as HTTPS. Leave it unset if you are not behind such a proxy, and confirm the app is reachable only through that proxy, not directly. Run `python manage.py check --deploy` and fix what it reports. - -## 3. Authentication - -Follow [authentication.md](authentication.md). Framework specifics: - -- Keep Django's built-in password hashing; do not replace it with custom code (this guide's advice). -- If your Flask or FastAPI application implements its own password accounts, provide its account storage and hash passwords with `argon2-cffi` or `bcrypt` (this guide's advice; follow the algorithm and legacy-bcrypt constraints in [authentication.md](authentication.md)): - -```python -from argon2 import PasswordHasher -ph = PasswordHasher() -hash_ = ph.hash(password) -ph.verify(hash_, password) # raises on mismatch -``` - -The [argon2-cffi 25.1.0 example](https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28) documents these `PasswordHasher`, `hash` and `verify` calls and a `VerifyMismatchError` on mismatch. The [pinned bcrypt example](https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76) documents `hashpw(password, gensalt())` and `checkpw(password, hashed)`; these API examples do not establish framework user-store capabilities. - -- Generate tokens and secrets with the standard library, and load them from the environment (this guide's advice): - -```python -import secrets -token = secrets.token_urlsafe(32) -``` - -In [CPython v3.14.8](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103), `token_urlsafe(nbytes=None)` returns URL-safe Base64 text from `nbytes` random bytes; the argument counts input bytes, not output characters. The explicit `32` above is this guide's example choice, not a size suitable for every secret. If the argument is omitted or `None`, it uses `DEFAULT_ENTROPY`, [32 bytes at this pin](https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45); the [documented default can change](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130), including in maintenance releases. - -- FastAPI's `fastapi.security` classes (`HTTPBearer`, `APIKeyHeader`, `OAuth2AuthorizationCodeBearer`, and so on) extract the credential from the request and declare the OpenAPI security scheme; what they check varies by helper (an API-key helper checks only that a value is present; the HTTP and OAuth2 bearer helpers may also check the scheme), but none authenticate the credential (no signature, issuer, audience, or expiry check), and `OpenIdConnect` is documented as a stub that does not implement the scheme or use the discovery URL. Use them to extract the token, then validate it (signature, issuer, audience, expiry) with an OIDC library such as Authlib, and authorize per [oidc-integration.md](oidc-integration.md). -- Rate-limit login routes (this guide's advice; for example with a proxy-level limit or a library such as slowapi for Starlette/FastAPI ASGI apps' HTTP endpoints; endpoints using slowapi's `limit` decorator must take an explicit `request` argument, and slowapi does not support WebSocket endpoints). -- MFA: add TOTP with [pyotp](https://github.com/pyauth/pyotp) plus the [qrcode](https://pypi.org/project/qrcode/) package for enrolment QR codes; [django-otp](https://pypi.org/project/django-otp/) integrates this into Django. Requirements and options in [mfa.md](mfa.md). - -## 4. Client-side TLS discipline - -Never ship `verify=False` (requests/httpx) or `ssl._create_unverified_context` (this guide's advice). For an internal CA, point the client at it instead (this guide's advice): - -```bash -export REQUESTS_CA_BUNDLE=/path/ca.crt # requests -export SSL_CERT_FILE=/path/ca.crt # HTTPX with verify=True and trust_env=True; OpenSSL default CA file for ssl when its default verify paths apply (see caveat below) -``` - -HTTPX 0.28.1's SSL guide says `SSL_CERT_FILE` and `SSL_CERT_DIR` are not automatically used and shows an explicit context; its code path contradicts that: its sync and async transports pass `verify` and `trust_env` to `create_ssl_context`, which reads `SSL_CERT_FILE` when both are true. Its environment guide says environment variables are used by default but does not list `SSL_CERT_FILE`. This guide's advice: use the export above for that HTTPX code path; pass an explicit CA context for other configurations. With no CA specified and verification enabled, CPython's `ssl.create_default_context()` tries to load default CAs (CPython notes this may fail silently). `ssl.get_default_verify_paths()` reports OpenSSL's environment key for a CA file; OpenSSL names `SSL_CERT_FILE` as an override for its default CA file. CPython's cited files do not name `SSL_CERT_FILE`, so whether a given `ssl` configuration uses that override remains unverified. - -## 5. Do not quick-share files with `http.server` - -`python -m http.server` is a common quick-share suggestion, and it is the wrong one on any reachable host. By default it binds every interface (as of Python 3.14.4), and it serves the current directory: source, `.env`, private keys, and database dumps are all downloadable by anyone who can reach the port. It also follows symbolic links, so a link in that directory hands out files from outside it, and the Python docs mark the module as not suitable for production. If you have no alternative, bind loopback and serve a directory that holds only what you mean to share: - -```bash -cd /path/to/a/directory/with/nothing/private || exit -python -m http.server -b 127.0.0.1 8000 -``` - -Reach it through an SSH tunnel, including one that runs over your tailnet, never a public bind. - -## 6. Verify (REASONED: TLS, authentication and listener expectations follow the Sources below and linked deployment guidance; no deployment run or outcome is recorded in this guide. This metadata-only review has no deployed Python application, proxy or managed ingress to test.) - -```bash -curl -q -sSI --noproxy '*' https://example.com/ # public HTTPS endpoint: must succeed without -k; if not, investigate the cert, chain, hostname, or your client trust store, never -k -# An unauthenticated request to a real protected path must be REJECTED and a valid credential ACCEPTED. --noproxy -# disables YOUR client proxy, not a reverse proxy in front of the app, so if the app sits behind an auth proxy this -# pair cannot tell whether the app or the proxy enforced it: confirm the layer by probing the app directly or in its logs. -curl -q -g -sS --noproxy '*' -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # no credentials: expect 401/403 -curl -q -g -sS --noproxy '*' -H @cred.txt -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # valid credential from a file (cred.txt holds a full header line, e.g. "Authorization: Bearer ..."): expect 2xx -ss -tlnp # every listener: loopback for a same-host reverse proxy, or the platform's required address and port from section 1 on managed ingress -``` - -## Sources (checked September 2026) - -- Python `http.server` (binds all interfaces by default, serves the current directory, follows symlinks, not for production) (Python 3.14.4): https://docs.python.org/3/library/http.server.html -- Gunicorn documentation (settings reference: bind, certfile, keyfile, ca_certs): https://gunicorn.org/reference/settings/ -- Uvicorn settings reference: https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md -- Django deployment checklist: https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ -- argon2-cffi: https://argon2-cffi.readthedocs.io/ -- argon2-cffi PasswordHasher, hash and verify mismatch example (25.1.0, checked October 2026): https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28 -- Python bcrypt hashpw, gensalt and checkpw example (commit 2b4ba9ac84df972e8e81311d09559af8dc82ef33, checked October 2026): https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76 -- Werkzeug serving (`ssl_context="adhoc"` requires cryptography): https://werkzeug.palletsprojects.com/en/stable/serving/ -- FastAPI security reference: https://fastapi.tiangolo.com/reference/security/ ; `OpenIdConnect` source (stub warning): https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py -- `http.server` binds with `bind=None` and `AI_PASSIVE`, the wildcard address (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334 -- `python -m http.server`'s `-b/--bind` has no default value, and its help says "default: all interfaces" (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373 -- Python 3.14 secrets.token_urlsafe (series documentation, checked October 2026): https://docs.python.org/3.14/library/secrets.html -- CPython token_urlsafe input bytes and Base64 output (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103 -- CPython secrets default entropy and token_bytes fallback (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45 -- CPython secrets default may change in maintenance releases (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130 -- Python 3.14 os.getenv environment loading (series documentation, checked October 2026): https://docs.python.org/3.14/library/os.html#os.getenv -- PyOTP one-time passwords and MFA (v2.9.0, checked October 2026): https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5 -- qrcode enrolment image generation (v8.2, checked October 2026): https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44 -- django-otp Django authentication integration (1.6.1, checked October 2026): https://pypi.org/project/django-otp/1.6.1/ -- Requests REQUESTS_CA_BUNDLE trust roots (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 -- slowapi Starlette/FastAPI scope (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3 -- slowapi limit decorator, endpoint request argument and WebSocket limitation (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50 -- HTTPX environment-variable default and trust_env (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7 -- HTTPX environment guide variable list, which omits SSL_CERT_FILE (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53 -- HTTPX SSL guide, verify=False example (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17 -- HTTPX SSL guide, conflicting SSL_CERT_FILE statement (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86 -- HTTPX SSL context selection with verify=True and trust_env=True (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40 -- HTTPX sync transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154 -- HTTPX async transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298 -- CPython ssl get_default_verify_paths (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405 -- CPython ssl load_default_certs (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548 -- CPython ssl create_default_context conditional default-CA load (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736 -- CPython ssl _create_unverified_context (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754 -- CPython ssl documentation for default verify paths and OpenSSL environment keys (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433 -- CPython ssl documentation for default CAs and silent failure (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649 -- OpenSSL default CA file environment override (openssl-4.0.3, checked October 2026): https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49 -- Requests verify=False disables certificate verification (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243 -- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services -- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing -- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles -- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations -- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 -- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 -- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 -- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 -- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 diff --git a/plugin/skills/secure-deployment-config/references/ruby.md b/plugin/skills/secure-deployment-config/references/ruby.md index a7f79c2e2..5fc745b6d 100644 --- a/plugin/skills/secure-deployment-config/references/ruby.md +++ b/plugin/skills/secure-deployment-config/references/ruby.md @@ -1,177 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "d7d88bbf754652ad64cd1044824c2cc41633732451a815523e2adabc25c7a2a4", - "components": { - "puma-readme": { - "name": "Puma README", - "basis": "aef89221e4d729c3133c723844382331ba3bbbd9", - "sources": { - "se5323687b58f": "https://github.com/puma/puma/blob/aef89221e4d729c3133c723844382331ba3bbbd9/README.md" - } - }, - "puma-dsl": { - "name": "Puma DSL", - "basis": "d70de8b4e926f1f5fa0269dc46cdfadf52562628", - "sources": { - "s8d510226544c": "https://github.com/puma/puma/blob/d70de8b4e926f1f5fa0269dc46cdfadf52562628/lib/puma/dsl.rb" - } - }, - "puma": { - "name": "Puma defaults", - "basis": "v8.0.2", - "sources": { - "s9687194339f3": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L369-L384", - "s8e2a34b121ba": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L174", - "s1d9011be835b": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/const.rb#L214-L215" - } - }, - "rails": { - "name": "Rails documentation", - "basis": "unknown", - "sources": { - "s0e5c35fd756d": "https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html", - "s22fb0dcbb6e1": "https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html", - "sba5f72c7a627": "https://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html", - "sae7bf5d75809": "https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html", - "s574629d873e4": "https://guides.rubyonrails.org/configuring.html", - "s2abb8f0ec8d2": "https://guides.rubyonrails.org/security.html" - } - }, - "assume": { - "name": "Action Pack", - "basis": "7.1", - "sources": { - "sf720dd92b2e7": "https://github.com/rails/rails/blob/ffcbf6f205363f8c2fb3e9834bc86690dd59f1cb/actionpack/CHANGELOG.md" - } - }, - "credentials": { - "name": "Rails credentials source", - "basis": "e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a", - "sources": { - "saab0a90edc0c": "https://github.com/rails/rails/blob/e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a/railties/lib/rails/commands/credentials/USAGE" - } - }, - "rack": { - "name": "Rack::Attack", - "basis": "unknown", - "sources": { - "s1e5ca83efb79": "https://github.com/rack/rack-attack" - } - }, - "sidekiq": { - "name": "Sidekiq Web", - "basis": "unknown", - "sources": { - "s106d0b1ced6e": "https://github.com/sidekiq/sidekiq/wiki/Monitoring" - } - }, - "net-http": { - "name": "Net::HTTP source", - "basis": "39cf5f648a9e5e25312909a2dc26681edaa2402c", - "sources": { - "sbcafa3e06a73": "https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb" - } - }, - "openssl-ruby": { - "name": "Ruby OpenSSL source", - "basis": "a77ed4b9908e179cea8f018f7e245535301746a8", - "sources": { - "sf0192683941c": "https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb" - } - }, - "openssl": { - "name": "OpenSSL documentation", - "basis": "unknown", - "sources": { - "se3858048f9d6": "https://docs.openssl.org/master/man7/openssl-env/" - } - } - }, - "claims": { - "puma-default": {"text": "Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0.", "components": ["puma"], "sources": ["puma:s9687194339f3", "puma:s8e2a34b121ba", "puma:s1d9011be835b"], "status": "REASONED"}, - "puma-private": {"text": "Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED"}, - "puma-tls": {"text": "Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates.", "components": ["puma-dsl"], "sources": ["puma-dsl:s8d510226544c"], "status": "REASONED"}, - "force-ssl": {"text": "force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED"}, - "assume-ssl": {"text": "Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP.", "components": ["rails", "assume"], "sources": ["rails:s574629d873e4", "assume:sf720dd92b2e7"], "status": "REASONED"}, - "hosts": {"text": "Add the application hostname to config.hosts for Host allowlisting.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, - "session": {"text": "CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry.", "components": ["rails"], "sources": ["rails:sba5f72c7a627", "rails:s574629d873e4"], "status": "REASONED"}, - "trusted-proxies": {"text": "Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits.", "components": ["rails"], "sources": ["rails:s22fb0dcbb6e1"], "status": "REASONED"}, - "same-site-default": {"text": "cookies_same_site_protection defaults lax from load_defaults 6.1.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, - "auth-generator": {"text": "Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, - "passwords": {"text": "has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length.", "components": ["rails"], "sources": ["rails:sae7bf5d75809"], "status": "REASONED"}, - "rate-limit": {"text": "Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store.", "components": ["rails"], "sources": ["rails:s0e5c35fd756d"], "status": "REASONED"}, - "rack-attack": {"text": "Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware.", "components": ["rack"], "sources": ["rack:s1e5ca83efb79"], "status": "REASONED"}, - "credentials": {"text": "Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores.", "components": ["credentials", "rails"], "sources": ["credentials:saab0a90edc0c", "rails:s2abb8f0ec8d2"], "status": "REASONED"}, - "require-key": {"text": "require_master_key=true refuses boot without the decryption key.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, - "mfa": {"text": "Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, - "sidekiq-open": {"text": "Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, - "sidekiq-devise": {"text": "Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, - "sidekiq-basic": {"text": "Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, - "client-tls": {"text": "Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE.", "components": ["net-http"], "sources": ["net-http:sbcafa3e06a73"], "status": "REASONED"}, - "client-store": {"text": "Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store.", "components": ["openssl-ruby", "openssl"], "sources": ["openssl-ruby:sf0192683941c", "openssl:se3858048f9d6"], "status": "REASONED"}, - "verify-bind": {"text": "Inventory every listener; Puma should bind only 127.0.0.1:3000.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED", "verify": [1]}, - "verify-https": {"text": "HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED", "verify": [1]}, - "verify-dashboard": {"text": "Anonymous dashboard request should return login 302 or 401.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED", "verify": [1]}, - "verify-sidekiq": {"text": "Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED", "verify": [1]}, - "verify-key": {"text": "git ls-files config/master.key should print nothing.", "components": ["credentials"], "sources": ["credentials:saab0a90edc0c"], "status": "REASONED", "verify": [1]} - } -} ---- # Ruby on Rails and Puma: 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-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): - -- puma-readme: Puma README (aef89221e4d729c3133c723844382331ba3bbbd9) -- puma-dsl: Puma DSL (d70de8b4e926f1f5fa0269dc46cdfadf52562628) -- puma: Puma defaults (v8.0.2) -- rails: Rails documentation (unknown) -- assume: Action Pack (7.1) -- credentials: Rails credentials source (e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a) -- rack: Rack::Attack (unknown) -- sidekiq: Sidekiq Web (unknown) -- net-http: Net::HTTP source (39cf5f648a9e5e25312909a2dc26681edaa2402c) -- openssl-ruby: Ruby OpenSSL source (a77ed4b9908e179cea8f018f7e245535301746a8) -- openssl: OpenSSL documentation (unknown) - -Claims (full source mapping in the front matter): +Written against Rails documentation (no recorded version), Action Pack 7.1, and Puma defaults v8.0.2; 5 further sources are pinned to a commit and 3 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 26 claims, all reasoned from cited sources. -- puma-default (REASONED; puma): Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0. -- puma-private (REASONED; puma-readme, puma-dsl): Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only. -- puma-tls (REASONED; puma-dsl): Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates. -- force-ssl (REASONED; rails): force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS. -- assume-ssl (REASONED; rails, assume): Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP. -- hosts (REASONED; rails): Add the application hostname to config.hosts for Host allowlisting. -- session (REASONED; rails): CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry. -- trusted-proxies (REASONED; rails): Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits. -- same-site-default (REASONED; rails): cookies_same_site_protection defaults lax from load_defaults 6.1. -- auth-generator (REASONED; rails): Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password. -- passwords (REASONED; rails): has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length. -- rate-limit (REASONED; rails): Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store. -- rack-attack (REASONED; rack): Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware. -- credentials (REASONED; credentials, rails): Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores. -- require-key (REASONED; rails): require_master_key=true refuses boot without the decryption key. -- mfa (REASONED; rails): Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance. -- sidekiq-open (REASONED; sidekiq): Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues. -- sidekiq-devise (REASONED; sidekiq): Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth. -- sidekiq-basic (REASONED; sidekiq): Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &. -- client-tls (REASONED; net-http): Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE. -- client-store (REASONED; openssl-ruby, openssl): Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store. -- verify-bind (REASONED; puma-readme, puma-dsl): Inventory every listener; Puma should bind only 127.0.0.1:3000. -- verify-https (REASONED; rails): HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies. -- verify-dashboard (REASONED; rails): Anonymous dashboard request should return login 302 or 401. -- verify-sidekiq (REASONED; sidekiq): Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success. -- verify-key (REASONED; credentials): git ls-files config/master.key should print nothing. - +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. Puma's default bind is `tcp://[::]:9292` when the host has a non-loopback IPv6 address, otherwise `tcp://0.0.0.0:9292` (as of v8.0.2): every interface, plain HTTP. Rails encrypts its session cookie but still sends it in clear unless HTTPS is enforced. Preferred layout: bind Puma to loopback and terminate TLS in a reverse proxy ([nginx.md](nginx.md), [caddy.md](caddy.md), [apache.md](apache.md)) or behind [cloudflare.md](cloudflare.md), with a certificate from [free-certificates.md](free-certificates.md). Puma can also terminate TLS itself, shown below. @@ -298,3 +129,198 @@ git ls-files config/master.key # prints nothin - Net::HTTP source (`verify_mode`, `ca_file`, `VERIFY_PEER` default): https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb - Ruby OpenSSL `SSLContext` defaults (`DEFAULT_CERT_STORE.set_default_paths`, `VERIFY_PEER`): https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb - OpenSSL environment variables (`SSL_CERT_FILE`, `SSL_CERT_DIR`): https://docs.openssl.org/master/man7/openssl-env/ + + + +## 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/ruby.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): + +- rails: Rails documentation (unknown) +- assume: Action Pack (7.1) +- puma: Puma defaults (v8.0.2) +- puma-readme: Puma README (aef89221e4d729c3133c723844382331ba3bbbd9) +- puma-dsl: Puma DSL (d70de8b4e926f1f5fa0269dc46cdfadf52562628) +- credentials: Rails credentials source (e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a) +- rack: Rack::Attack (unknown) +- sidekiq: Sidekiq Web (unknown) +- net-http: Net::HTTP source (39cf5f648a9e5e25312909a2dc26681edaa2402c) +- openssl-ruby: Ruby OpenSSL source (a77ed4b9908e179cea8f018f7e245535301746a8) +- openssl: OpenSSL documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- puma-default (REASONED; puma): Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0. +- puma-private (REASONED; puma-readme, puma-dsl): Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only. +- puma-tls (REASONED; puma-dsl): Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates. +- force-ssl (REASONED; rails): force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS. +- assume-ssl (REASONED; rails, assume): Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP. +- hosts (REASONED; rails): Add the application hostname to config.hosts for Host allowlisting. +- session (REASONED; rails): CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry. +- trusted-proxies (REASONED; rails): Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits. +- same-site-default (REASONED; rails): cookies_same_site_protection defaults lax from load_defaults 6.1. +- auth-generator (REASONED; rails): Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password. +- passwords (REASONED; rails): has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length. +- rate-limit (REASONED; rails): Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store. +- rack-attack (REASONED; rack): Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware. +- credentials (REASONED; credentials, rails): Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores. +- require-key (REASONED; rails): require_master_key=true refuses boot without the decryption key. +- mfa (REASONED; rails): Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance. +- sidekiq-open (REASONED; sidekiq): Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues. +- sidekiq-devise (REASONED; sidekiq): Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth. +- sidekiq-basic (REASONED; sidekiq): Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &. +- client-tls (REASONED; net-http): Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE. +- client-store (REASONED; openssl-ruby, openssl): Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store. +- verify-bind (REASONED; puma-readme, puma-dsl): Inventory every listener; Puma should bind only 127.0.0.1:3000. +- verify-https (REASONED; rails): HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies. +- verify-dashboard (REASONED; rails): Anonymous dashboard request should return login 302 or 401. +- verify-sidekiq (REASONED; sidekiq): Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success. +- verify-key (REASONED; credentials): git ls-files config/master.key should print nothing. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "d7d88bbf754652ad64cd1044824c2cc41633732451a815523e2adabc25c7a2a4", + "components": { + "rails": { + "name": "Rails documentation", + "basis": "unknown", + "sources": { + "s0e5c35fd756d": "https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html", + "s22fb0dcbb6e1": "https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html", + "sba5f72c7a627": "https://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html", + "sae7bf5d75809": "https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html", + "s574629d873e4": "https://guides.rubyonrails.org/configuring.html", + "s2abb8f0ec8d2": "https://guides.rubyonrails.org/security.html" + } + }, + "assume": { + "name": "Action Pack", + "basis": "7.1", + "sources": { + "sf720dd92b2e7": "https://github.com/rails/rails/blob/ffcbf6f205363f8c2fb3e9834bc86690dd59f1cb/actionpack/CHANGELOG.md" + } + }, + "puma": { + "name": "Puma defaults", + "basis": "v8.0.2", + "sources": { + "s9687194339f3": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L369-L384", + "s8e2a34b121ba": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L174", + "s1d9011be835b": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/const.rb#L214-L215" + } + }, + "puma-readme": { + "name": "Puma README", + "basis": "aef89221e4d729c3133c723844382331ba3bbbd9", + "sources": { + "se5323687b58f": "https://github.com/puma/puma/blob/aef89221e4d729c3133c723844382331ba3bbbd9/README.md" + } + }, + "puma-dsl": { + "name": "Puma DSL", + "basis": "d70de8b4e926f1f5fa0269dc46cdfadf52562628", + "sources": { + "s8d510226544c": "https://github.com/puma/puma/blob/d70de8b4e926f1f5fa0269dc46cdfadf52562628/lib/puma/dsl.rb" + } + }, + "credentials": { + "name": "Rails credentials source", + "basis": "e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a", + "sources": { + "saab0a90edc0c": "https://github.com/rails/rails/blob/e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a/railties/lib/rails/commands/credentials/USAGE" + } + }, + "rack": { + "name": "Rack::Attack", + "basis": "unknown", + "sources": { + "s1e5ca83efb79": "https://github.com/rack/rack-attack" + } + }, + "sidekiq": { + "name": "Sidekiq Web", + "basis": "unknown", + "sources": { + "s106d0b1ced6e": "https://github.com/sidekiq/sidekiq/wiki/Monitoring" + } + }, + "net-http": { + "name": "Net::HTTP source", + "basis": "39cf5f648a9e5e25312909a2dc26681edaa2402c", + "sources": { + "sbcafa3e06a73": "https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb" + } + }, + "openssl-ruby": { + "name": "Ruby OpenSSL source", + "basis": "a77ed4b9908e179cea8f018f7e245535301746a8", + "sources": { + "sf0192683941c": "https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "unknown", + "sources": { + "se3858048f9d6": "https://docs.openssl.org/master/man7/openssl-env/" + } + } + }, + "claims": { + "puma-default": {"text": "Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0.", "components": ["puma"], "sources": ["puma:s9687194339f3", "puma:s8e2a34b121ba", "puma:s1d9011be835b"], "status": "REASONED"}, + "puma-private": {"text": "Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED"}, + "puma-tls": {"text": "Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates.", "components": ["puma-dsl"], "sources": ["puma-dsl:s8d510226544c"], "status": "REASONED"}, + "force-ssl": {"text": "force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED"}, + "assume-ssl": {"text": "Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP.", "components": ["rails", "assume"], "sources": ["rails:s574629d873e4", "assume:sf720dd92b2e7"], "status": "REASONED"}, + "hosts": {"text": "Add the application hostname to config.hosts for Host allowlisting.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "session": {"text": "CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry.", "components": ["rails"], "sources": ["rails:sba5f72c7a627", "rails:s574629d873e4"], "status": "REASONED"}, + "trusted-proxies": {"text": "Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits.", "components": ["rails"], "sources": ["rails:s22fb0dcbb6e1"], "status": "REASONED"}, + "same-site-default": {"text": "cookies_same_site_protection defaults lax from load_defaults 6.1.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "auth-generator": {"text": "Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "passwords": {"text": "has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length.", "components": ["rails"], "sources": ["rails:sae7bf5d75809"], "status": "REASONED"}, + "rate-limit": {"text": "Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store.", "components": ["rails"], "sources": ["rails:s0e5c35fd756d"], "status": "REASONED"}, + "rack-attack": {"text": "Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware.", "components": ["rack"], "sources": ["rack:s1e5ca83efb79"], "status": "REASONED"}, + "credentials": {"text": "Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores.", "components": ["credentials", "rails"], "sources": ["credentials:saab0a90edc0c", "rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "require-key": {"text": "require_master_key=true refuses boot without the decryption key.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "mfa": {"text": "Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "sidekiq-open": {"text": "Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "sidekiq-devise": {"text": "Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "sidekiq-basic": {"text": "Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "client-tls": {"text": "Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE.", "components": ["net-http"], "sources": ["net-http:sbcafa3e06a73"], "status": "REASONED"}, + "client-store": {"text": "Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store.", "components": ["openssl-ruby", "openssl"], "sources": ["openssl-ruby:sf0192683941c", "openssl:se3858048f9d6"], "status": "REASONED"}, + "verify-bind": {"text": "Inventory every listener; Puma should bind only 127.0.0.1:3000.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED", "verify": [1]}, + "verify-https": {"text": "HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED", "verify": [1]}, + "verify-dashboard": {"text": "Anonymous dashboard request should return login 302 or 401.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED", "verify": [1]}, + "verify-sidekiq": {"text": "Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED", "verify": [1]}, + "verify-key": {"text": "git ls-files config/master.key should print nothing.", "components": ["credentials"], "sources": ["credentials:saab0a90edc0c"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/server-admin-panels.md b/plugin/skills/secure-deployment-config/references/server-admin-panels.md index 339a764a3..647098f59 100644 --- a/plugin/skills/secure-deployment-config/references/server-admin-panels.md +++ b/plugin/skills/secure-deployment-config/references/server-admin-panels.md @@ -1,203 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "3478bd2ff9b8156ba6721d2d02631c39a01142288dfa2d17f860b649772c131f", - "components": { - "cockpit": { - "name": "Cockpit", - "basis": "368", - "sources": { - "sc42ba09017c7": "https://github.com/cockpit-project/cockpit/blob/368/src/systemd/cockpit.socket.in", - "s0c1e0db624a4": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/listen.adoc", - "sd0cb4ea3db56": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/https.adoc", - "sf157ab3a6f47": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/man/pages/cockpit.conf.5.adoc", - "s48ebdec28e10": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/authentication.adoc", - "s2432e599485f": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/privileges.adoc", - "sbdcd6e74a0c7": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.pam", - "sc20103cfb39b": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.spec" - } - }, - "systemd": { - "name": "systemd", - "basis": "v262", - "sources": { - "s0a96457c713e": "https://github.com/systemd/systemd/blob/v262/man/systemd.socket.xml" - } - }, - "webmin": { - "name": "Webmin", - "basis": "2.670", - "sources": { - "sa4f7ac343e85": "https://github.com/webmin/webmin/blob/2.670/setup.sh", - "sb8fec2baa0f2": "https://github.com/webmin/webmin/blob/2.670/miniserv.pl", - "sdd465cac1ff8": "https://github.com/webmin/webmin/blob/2.670/makedebian.pl", - "s9ecba8f15014": "https://github.com/webmin/webmin/blob/2.670/makerpm.pl", - "sdb2ef2bf9978": "https://github.com/webmin/webmin/blob/2.670/miniserv-lib.pl", - "s1a1dbd21dddd": "https://github.com/webmin/webmin/blob/2.670/shell/defaultacl", - "s2f3504ad1328": "https://github.com/webmin/webmin/blob/2.670/webmin/twofactor-funcs-lib.pl" - } - }, - "webmin-docs": { - "name": "Webmin docs", - "basis": "8ceae26c5a074053905cbcc6c0053be573633f3a", - "sources": { - "s7a91f9f23044": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/docs/Modules/webmin-configuration.md", - "s8b072a3403d1": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/security.md" - } - }, - "usermin": { - "name": "Usermin", - "basis": "2.570", - "sources": { - "s7fe74c9a3138": "https://github.com/webmin/usermin/blob/2.570/setup.sh" - } - }, - "pve-docs": { - "name": "Proxmox VE 9.2 docs", - "basis": "9.2.12", - "sources": { - "s8538bab3d3fc": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveproxy.adoc", - "sdbc681682e8f": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveum.adoc", - "sc6bbbfe64d09": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pve-firewall.adoc", - "s31f242f10eaa": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=certificate-management.adoc", - "s954445cf4e14": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=debian/changelog" - } - }, - "pve-manager": { - "name": "pve-manager", - "basis": "9.2.20", - "sources": { - "se4c11b901cf8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/API2/Nodes.pm", - "s1507ce3e40b8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/changelog", - "s67c1a8621e22": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/control", - "sb7f08e4e98ab": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/pveproxy.pm", - "s0b79b5bbd95f": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm" - } - }, - "pve-http": { - "name": "Proxmox HTTP server", - "basis": "5119ff9bec08c69584c0c98bea3edd0098179e5f", - "sources": { - "s753d88abe320": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/AnyEvent.pm", - "sb8d499c6bd8c": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=debian/changelog", - "s2f151d3facd0": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm" - } - }, - "pve-common": { - "name": "libpve-common-perl 9.1.21", - "basis": "5054082fe492429fc37574985c2ca812af9a3125", - "sources": { - "s062026297433": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=debian/changelog", - "s022a9e4640ab": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm" - } - } - }, - "claims": { - "private": {"text": "Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor.", "components": ["cockpit", "webmin-docs", "pve-docs"], "sources": ["cockpit:s0c1e0db624a4", "webmin-docs:s7a91f9f23044", "pve-docs:s8538bab3d3fc", "pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, - "cockpit-bind": {"text": "Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses.", "components": ["cockpit", "systemd"], "sources": ["cockpit:sc42ba09017c7", "systemd:s0a96457c713e"], "status": "REASONED"}, - "cockpit-dropin": {"text": "Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart.", "components": ["cockpit"], "sources": ["cockpit:s0c1e0db624a4"], "status": "REASONED"}, - "cockpit-tls": {"text": "One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation.", "components": ["cockpit"], "sources": ["cockpit:sd0cb4ea3db56", "cockpit:sf157ab3a6f47"], "status": "REASONED"}, - "cockpit-pam": {"text": "Local accounts use Cockpit PAM with normal SSH-equivalent privileges.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, - "cockpit-root": {"text": "First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable.", "components": ["cockpit"], "sources": ["cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, - "cockpit-escalation": {"text": "Users allowed sudo/polkit escalation get an immediately elevated session and terminal.", "components": ["cockpit"], "sources": ["cockpit:s2432e599485f"], "status": "REASONED"}, - "cockpit-mfa": {"text": "No built-in second factor; use PAM, Kerberos or client certificates.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, - "cockpit-rate": {"text": "MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, - "cockpit-loginto": {"text": "Set LoginTo=false to prevent unauthenticated scanning of reachable private networks.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, - "webmin-bind": {"text": "Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, - "webmin-discovery": {"text": "Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, - "webmin-access": {"text": "Set management bind and allow networks, then restart; empty allow admits all except denied clients.", "components": ["webmin", "webmin-docs"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978", "webmin-docs:s7a91f9f23044"], "status": "REASONED"}, - "webmin-tls": {"text": "Deb/RPM packages enable ssl=1 with a self-signed certificate.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdd465cac1ff8", "webmin:s9ecba8f15014"], "status": "REASONED"}, - "webmin-root": {"text": "Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:s1a1dbd21dddd"], "status": "REASONED"}, - "webmin-sudo": {"text": "Installed sudo=1 lets sudo-permitted Unix users log in effectively as root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, - "webmin-mfa": {"text": "TOTP/Authy exists but is not configured at install; enable and enrol every user.", "components": ["webmin"], "sources": ["webmin:s2f3504ad1328"], "status": "REASONED"}, - "webmin-advisories": {"text": "Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release.", "components": ["webmin-docs"], "sources": ["webmin-docs:s8b072a3403d1"], "status": "REASONED"}, - "webmin-rate": {"text": "Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, - "pve-bind": {"text": "pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab"], "status": "REASONED"}, - "pve-cluster": {"text": "The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules.", "components": ["pve-docs"], "sources": ["pve-docs:s8538bab3d3fc"], "status": "REASONED"}, - "pve-acl": {"text": "Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone.", "components": ["pve-docs", "pve-manager", "pve-http"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-http:s753d88abe320"], "status": "REASONED"}, - "pve-firewall": {"text": "Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses.", "components": ["pve-docs"], "sources": ["pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, - "pve-root": {"text": "root@pam can always log in as unconfined admin; installer sets the web-interface root password.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "pve-shell": {"text": "Node shell opens /bin/login -f root without another password.", "components": ["pve-manager"], "sources": ["pve-manager:se4c11b901cf8"], "status": "REASONED"}, - "pve-mfa": {"text": "Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "pve-lockout": {"text": "Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "pve-password-delay": {"text": "Unauthorized API responses delay three seconds; this is not password lockout.", "components": ["pve-http"], "sources": ["pve-http:s753d88abe320"], "status": "REASONED"}, - "pve-certificates": {"text": "Each cluster creates its own self-signed CA.", "components": ["pve-docs"], "sources": ["pve-docs:s31f242f10eaa"], "status": "REASONED"}, - "pve-tokens": {"text": "API tokens default to separated privileges and cannot access VM/node consoles.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "verify-cockpit": {"text": "Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults.", "components": ["cockpit"], "sources": ["cockpit:sc42ba09017c7", "cockpit:s0c1e0db624a4", "cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, - "verify-pve": {"text": "Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-docs:sdbc681682e8f", "pve-docs:sc6bbbfe64d09", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED"}, - "verify-external": {"text": "Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "REASONED"}, - "webmin-loopback": {"text": "Loopback bind with no listen line produced one TCP socket and no UDP.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2"], "status": "DEMONSTRATED", "evidence": "its only socket was TCP `127.0.0.1` on the test port, with no UDP socket."}, - "webmin-rate-run": {"text": "Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "the fifth wrong password was delayed past a five-second client timeout by miniserv's growing failure delay, and the right password was then refused with `403` \"Access denied for 127.0.0.1\"."}, - "verify-listeners": {"text": "Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host.", "components": ["cockpit", "webmin", "usermin", "pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED", "verify": [1]}, - "verify-webmin-parser": {"text": "Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "On each, the copy's parsed values matched Webmin 2.670's own `read_config_file` run on the same file, and the block printed the outcomes above; it reported an unreadable path as not checked.", "verify": [2]}, - "verify-tcp-loopback": {"text": "Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "DEMONSTRATED", "evidence": "loopback addresses pass, and hex spellings such as `::ffff:0:0` connected to a listener bound to 127.0.0.1", "verify": [3]} - } -} ---- # Server administration panels: Cockpit, Webmin, and Proxmox VE - -**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 Cockpit 368, Webmin 2.670, Proxmox VE 9.2 docs 9.2.12, pve-manager 9.2.20, Usermin 2.570, and systemd v262; 3 further sources are pinned to a commit. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 38 claims: 4 demonstrated by a check that ran and 34 reasoned from cited sources. -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): - -- cockpit: Cockpit (368) -- systemd: systemd (v262) -- webmin: Webmin (2.670) -- webmin-docs: Webmin docs (8ceae26c5a074053905cbcc6c0053be573633f3a) -- usermin: Usermin (2.570) -- pve-docs: Proxmox VE 9.2 docs (9.2.12) -- pve-manager: pve-manager (9.2.20) -- pve-http: Proxmox HTTP server (5119ff9bec08c69584c0c98bea3edd0098179e5f) -- pve-common: libpve-common-perl 9.1.21 (5054082fe492429fc37574985c2ca812af9a3125) - -Claims (full source mapping in the front matter): - -- private (REASONED; cockpit, webmin-docs, pve-docs): Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor. -- cockpit-bind (REASONED; cockpit, systemd): Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses. -- cockpit-dropin (REASONED; cockpit): Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart. -- cockpit-tls (REASONED; cockpit): One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation. -- cockpit-pam (REASONED; cockpit): Local accounts use Cockpit PAM with normal SSH-equivalent privileges. -- cockpit-root (REASONED; cockpit): First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable. -- cockpit-escalation (REASONED; cockpit): Users allowed sudo/polkit escalation get an immediately elevated session and terminal. -- cockpit-mfa (REASONED; cockpit): No built-in second factor; use PAM, Kerberos or client certificates. -- cockpit-rate (REASONED; cockpit): MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout. -- cockpit-loginto (REASONED; cockpit): Set LoginTo=false to prevent unauthenticated scanning of reachable private networks. -- webmin-bind (REASONED; webmin, usermin): Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses. -- webmin-discovery (REASONED; webmin, usermin): Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused. -- webmin-access (REASONED; webmin, webmin-docs): Set management bind and allow networks, then restart; empty allow admits all except denied clients. -- webmin-tls (REASONED; webmin): Deb/RPM packages enable ssl=1 with a self-signed certificate. -- webmin-root (REASONED; webmin): Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root. -- webmin-sudo (REASONED; webmin): Installed sudo=1 lets sudo-permitted Unix users log in effectively as root. -- webmin-mfa (REASONED; webmin): TOTP/Authy exists but is not configured at install; enable and enrol every user. -- webmin-advisories (REASONED; webmin-docs): Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release. -- webmin-rate (REASONED; webmin): Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1. -- pve-bind (REASONED; pve-docs, pve-manager, pve-http, pve-common): pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified. -- pve-cluster (REASONED; pve-docs): The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules. -- pve-acl (REASONED; pve-docs, pve-manager, pve-http): Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone. -- pve-firewall (REASONED; pve-docs): Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses. -- pve-root (REASONED; pve-docs): root@pam can always log in as unconfined admin; installer sets the web-interface root password. -- pve-shell (REASONED; pve-manager): Node shell opens /bin/login -f root without another password. -- pve-mfa (REASONED; pve-docs): Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor. -- pve-lockout (REASONED; pve-docs): Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts. -- pve-password-delay (REASONED; pve-http): Unauthorized API responses delay three seconds; this is not password lockout. -- pve-certificates (REASONED; pve-docs): Each cluster creates its own self-signed CA. -- pve-tokens (REASONED; pve-docs): API tokens default to separated privileges and cannot access VM/node consoles. -- verify-cockpit (REASONED; cockpit): Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults. -- verify-pve (REASONED; pve-docs, pve-manager, pve-http, pve-common): Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings. -- verify-external (REASONED; cockpit, webmin, usermin, pve-docs): Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP. -- webmin-loopback (DEMONSTRATED; webmin): Loopback bind with no listen line produced one TCP socket and no UDP. -- webmin-rate-run (DEMONSTRATED; webmin): Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403. -- verify-listeners (REASONED; cockpit, webmin, usermin, pve-docs, pve-manager, pve-http, pve-common): Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host. -- verify-webmin-parser (DEMONSTRATED; webmin): Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof. -- verify-tcp-loopback (DEMONSTRATED; cockpit, webmin, usermin, pve-docs): Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard. - +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 server administration panel is root on the host behind a login form. Cockpit gives any user who can escalate with sudo or polkit an administrative session and a terminal. Webmin's packages make the host's @@ -512,3 +317,224 @@ give the server's public address. A "connected" on a port you did not mean to ex - pve-manager 9.2.20 spiceproxy shared configuration, bind and ACLs, lines 31 to 59 (checked October 2026): https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm - libpve-http-server-perl 6.0.5 configuration reader, lines 10 to 97, at 5119ff9bec08c69584c0c98bea3edd0098179e5f (checked October 2026): https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm - libpve-common-perl 9.1.21 socket creation and reuse, lines 803 to 854, at 5054082fe492429fc37574985c2ca812af9a3125 (checked October 2026): https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm + + + +## 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/server-admin-panels.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): + +- cockpit: Cockpit (368) +- webmin: Webmin (2.670) +- pve-docs: Proxmox VE 9.2 docs (9.2.12) +- pve-manager: pve-manager (9.2.20) +- webmin-docs: Webmin docs (8ceae26c5a074053905cbcc6c0053be573633f3a) +- pve-http: Proxmox HTTP server (5119ff9bec08c69584c0c98bea3edd0098179e5f) +- pve-common: libpve-common-perl 9.1.21 (5054082fe492429fc37574985c2ca812af9a3125) +- usermin: Usermin (2.570) +- systemd: systemd (v262) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; cockpit, webmin-docs, pve-docs): Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor. +- cockpit-bind (REASONED; cockpit, systemd): Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses. +- cockpit-dropin (REASONED; cockpit): Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart. +- cockpit-tls (REASONED; cockpit): One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation. +- cockpit-pam (REASONED; cockpit): Local accounts use Cockpit PAM with normal SSH-equivalent privileges. +- cockpit-root (REASONED; cockpit): First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable. +- cockpit-escalation (REASONED; cockpit): Users allowed sudo/polkit escalation get an immediately elevated session and terminal. +- cockpit-mfa (REASONED; cockpit): No built-in second factor; use PAM, Kerberos or client certificates. +- cockpit-rate (REASONED; cockpit): MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout. +- cockpit-loginto (REASONED; cockpit): Set LoginTo=false to prevent unauthenticated scanning of reachable private networks. +- webmin-bind (REASONED; webmin, usermin): Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses. +- webmin-discovery (REASONED; webmin, usermin): Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused. +- webmin-access (REASONED; webmin, webmin-docs): Set management bind and allow networks, then restart; empty allow admits all except denied clients. +- webmin-tls (REASONED; webmin): Deb/RPM packages enable ssl=1 with a self-signed certificate. +- webmin-root (REASONED; webmin): Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root. +- webmin-sudo (REASONED; webmin): Installed sudo=1 lets sudo-permitted Unix users log in effectively as root. +- webmin-mfa (REASONED; webmin): TOTP/Authy exists but is not configured at install; enable and enrol every user. +- webmin-advisories (REASONED; webmin-docs): Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release. +- webmin-rate (REASONED; webmin): Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1. +- pve-bind (REASONED; pve-docs, pve-manager, pve-http, pve-common): pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified. +- pve-cluster (REASONED; pve-docs): The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules. +- pve-acl (REASONED; pve-docs, pve-manager, pve-http): Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone. +- pve-firewall (REASONED; pve-docs): Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses. +- pve-root (REASONED; pve-docs): root@pam can always log in as unconfined admin; installer sets the web-interface root password. +- pve-shell (REASONED; pve-manager): Node shell opens /bin/login -f root without another password. +- pve-mfa (REASONED; pve-docs): Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor. +- pve-lockout (REASONED; pve-docs): Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts. +- pve-password-delay (REASONED; pve-http): Unauthorized API responses delay three seconds; this is not password lockout. +- pve-certificates (REASONED; pve-docs): Each cluster creates its own self-signed CA. +- pve-tokens (REASONED; pve-docs): API tokens default to separated privileges and cannot access VM/node consoles. +- verify-cockpit (REASONED; cockpit): Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults. +- verify-pve (REASONED; pve-docs, pve-manager, pve-http, pve-common): Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings. +- verify-external (REASONED; cockpit, webmin, usermin, pve-docs): Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP. +- webmin-loopback (DEMONSTRATED; webmin): Loopback bind with no listen line produced one TCP socket and no UDP. +- webmin-rate-run (DEMONSTRATED; webmin): Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403. +- verify-listeners (REASONED; cockpit, webmin, usermin, pve-docs, pve-manager, pve-http, pve-common): Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host. +- verify-webmin-parser (DEMONSTRATED; webmin): Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof. +- verify-tcp-loopback (DEMONSTRATED; cockpit, webmin, usermin, pve-docs): Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "3478bd2ff9b8156ba6721d2d02631c39a01142288dfa2d17f860b649772c131f", + "components": { + "cockpit": { + "name": "Cockpit", + "basis": "368", + "sources": { + "sc42ba09017c7": "https://github.com/cockpit-project/cockpit/blob/368/src/systemd/cockpit.socket.in", + "s0c1e0db624a4": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/listen.adoc", + "sd0cb4ea3db56": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/https.adoc", + "sf157ab3a6f47": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/man/pages/cockpit.conf.5.adoc", + "s48ebdec28e10": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/authentication.adoc", + "s2432e599485f": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/privileges.adoc", + "sbdcd6e74a0c7": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.pam", + "sc20103cfb39b": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.spec" + } + }, + "webmin": { + "name": "Webmin", + "basis": "2.670", + "sources": { + "sa4f7ac343e85": "https://github.com/webmin/webmin/blob/2.670/setup.sh", + "sb8fec2baa0f2": "https://github.com/webmin/webmin/blob/2.670/miniserv.pl", + "sdd465cac1ff8": "https://github.com/webmin/webmin/blob/2.670/makedebian.pl", + "s9ecba8f15014": "https://github.com/webmin/webmin/blob/2.670/makerpm.pl", + "sdb2ef2bf9978": "https://github.com/webmin/webmin/blob/2.670/miniserv-lib.pl", + "s1a1dbd21dddd": "https://github.com/webmin/webmin/blob/2.670/shell/defaultacl", + "s2f3504ad1328": "https://github.com/webmin/webmin/blob/2.670/webmin/twofactor-funcs-lib.pl" + } + }, + "pve-docs": { + "name": "Proxmox VE 9.2 docs", + "basis": "9.2.12", + "sources": { + "s8538bab3d3fc": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveproxy.adoc", + "sdbc681682e8f": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveum.adoc", + "sc6bbbfe64d09": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pve-firewall.adoc", + "s31f242f10eaa": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=certificate-management.adoc", + "s954445cf4e14": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=debian/changelog" + } + }, + "pve-manager": { + "name": "pve-manager", + "basis": "9.2.20", + "sources": { + "se4c11b901cf8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/API2/Nodes.pm", + "s1507ce3e40b8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/changelog", + "s67c1a8621e22": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/control", + "sb7f08e4e98ab": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/pveproxy.pm", + "s0b79b5bbd95f": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm" + } + }, + "webmin-docs": { + "name": "Webmin docs", + "basis": "8ceae26c5a074053905cbcc6c0053be573633f3a", + "sources": { + "s7a91f9f23044": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/docs/Modules/webmin-configuration.md", + "s8b072a3403d1": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/security.md" + } + }, + "pve-http": { + "name": "Proxmox HTTP server", + "basis": "5119ff9bec08c69584c0c98bea3edd0098179e5f", + "sources": { + "s753d88abe320": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/AnyEvent.pm", + "sb8d499c6bd8c": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=debian/changelog", + "s2f151d3facd0": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm" + } + }, + "pve-common": { + "name": "libpve-common-perl 9.1.21", + "basis": "5054082fe492429fc37574985c2ca812af9a3125", + "sources": { + "s062026297433": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=debian/changelog", + "s022a9e4640ab": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm" + } + }, + "usermin": { + "name": "Usermin", + "basis": "2.570", + "sources": { + "s7fe74c9a3138": "https://github.com/webmin/usermin/blob/2.570/setup.sh" + } + }, + "systemd": { + "name": "systemd", + "basis": "v262", + "sources": { + "s0a96457c713e": "https://github.com/systemd/systemd/blob/v262/man/systemd.socket.xml" + } + } + }, + "claims": { + "private": {"text": "Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor.", "components": ["cockpit", "webmin-docs", "pve-docs"], "sources": ["cockpit:s0c1e0db624a4", "webmin-docs:s7a91f9f23044", "pve-docs:s8538bab3d3fc", "pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, + "cockpit-bind": {"text": "Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses.", "components": ["cockpit", "systemd"], "sources": ["cockpit:sc42ba09017c7", "systemd:s0a96457c713e"], "status": "REASONED"}, + "cockpit-dropin": {"text": "Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart.", "components": ["cockpit"], "sources": ["cockpit:s0c1e0db624a4"], "status": "REASONED"}, + "cockpit-tls": {"text": "One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation.", "components": ["cockpit"], "sources": ["cockpit:sd0cb4ea3db56", "cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "cockpit-pam": {"text": "Local accounts use Cockpit PAM with normal SSH-equivalent privileges.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, + "cockpit-root": {"text": "First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable.", "components": ["cockpit"], "sources": ["cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, + "cockpit-escalation": {"text": "Users allowed sudo/polkit escalation get an immediately elevated session and terminal.", "components": ["cockpit"], "sources": ["cockpit:s2432e599485f"], "status": "REASONED"}, + "cockpit-mfa": {"text": "No built-in second factor; use PAM, Kerberos or client certificates.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, + "cockpit-rate": {"text": "MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "cockpit-loginto": {"text": "Set LoginTo=false to prevent unauthenticated scanning of reachable private networks.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "webmin-bind": {"text": "Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, + "webmin-discovery": {"text": "Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, + "webmin-access": {"text": "Set management bind and allow networks, then restart; empty allow admits all except denied clients.", "components": ["webmin", "webmin-docs"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978", "webmin-docs:s7a91f9f23044"], "status": "REASONED"}, + "webmin-tls": {"text": "Deb/RPM packages enable ssl=1 with a self-signed certificate.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdd465cac1ff8", "webmin:s9ecba8f15014"], "status": "REASONED"}, + "webmin-root": {"text": "Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:s1a1dbd21dddd"], "status": "REASONED"}, + "webmin-sudo": {"text": "Installed sudo=1 lets sudo-permitted Unix users log in effectively as root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, + "webmin-mfa": {"text": "TOTP/Authy exists but is not configured at install; enable and enrol every user.", "components": ["webmin"], "sources": ["webmin:s2f3504ad1328"], "status": "REASONED"}, + "webmin-advisories": {"text": "Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release.", "components": ["webmin-docs"], "sources": ["webmin-docs:s8b072a3403d1"], "status": "REASONED"}, + "webmin-rate": {"text": "Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, + "pve-bind": {"text": "pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab"], "status": "REASONED"}, + "pve-cluster": {"text": "The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules.", "components": ["pve-docs"], "sources": ["pve-docs:s8538bab3d3fc"], "status": "REASONED"}, + "pve-acl": {"text": "Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone.", "components": ["pve-docs", "pve-manager", "pve-http"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-http:s753d88abe320"], "status": "REASONED"}, + "pve-firewall": {"text": "Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses.", "components": ["pve-docs"], "sources": ["pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, + "pve-root": {"text": "root@pam can always log in as unconfined admin; installer sets the web-interface root password.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-shell": {"text": "Node shell opens /bin/login -f root without another password.", "components": ["pve-manager"], "sources": ["pve-manager:se4c11b901cf8"], "status": "REASONED"}, + "pve-mfa": {"text": "Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-lockout": {"text": "Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-password-delay": {"text": "Unauthorized API responses delay three seconds; this is not password lockout.", "components": ["pve-http"], "sources": ["pve-http:s753d88abe320"], "status": "REASONED"}, + "pve-certificates": {"text": "Each cluster creates its own self-signed CA.", "components": ["pve-docs"], "sources": ["pve-docs:s31f242f10eaa"], "status": "REASONED"}, + "pve-tokens": {"text": "API tokens default to separated privileges and cannot access VM/node consoles.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "verify-cockpit": {"text": "Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults.", "components": ["cockpit"], "sources": ["cockpit:sc42ba09017c7", "cockpit:s0c1e0db624a4", "cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, + "verify-pve": {"text": "Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-docs:sdbc681682e8f", "pve-docs:sc6bbbfe64d09", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED"}, + "verify-external": {"text": "Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "REASONED"}, + "webmin-loopback": {"text": "Loopback bind with no listen line produced one TCP socket and no UDP.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2"], "status": "DEMONSTRATED", "evidence": "its only socket was TCP `127.0.0.1` on the test port, with no UDP socket."}, + "webmin-rate-run": {"text": "Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "the fifth wrong password was delayed past a five-second client timeout by miniserv's growing failure delay, and the right password was then refused with `403` \"Access denied for 127.0.0.1\"."}, + "verify-listeners": {"text": "Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host.", "components": ["cockpit", "webmin", "usermin", "pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED", "verify": [1]}, + "verify-webmin-parser": {"text": "Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "On each, the copy's parsed values matched Webmin 2.670's own `read_config_file` run on the same file, and the block printed the outcomes above; it reported an unreadable path as not checked.", "verify": [2]}, + "verify-tcp-loopback": {"text": "Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "DEMONSTRATED", "evidence": "loopback addresses pass, and hex spellings such as `::ffff:0:0` connected to a listener bound to 127.0.0.1", "verify": [3]} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/sqlite-http-frontends.md b/plugin/skills/secure-deployment-config/references/sqlite-http-frontends.md index 0453b1cf9..5155c56c4 100644 --- a/plugin/skills/secure-deployment-config/references/sqlite-http-frontends.md +++ b/plugin/skills/secure-deployment-config/references/sqlite-http-frontends.md @@ -1,338 +1,12 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "cd2fba66ff332b791a1934a25ece6c5ef7f76fc791a8a4019819189f360eab0b", - "components": { - "ds-container": { - "name": "Datasette container generator", - "basis": "caf238aac86ebe370959b25384d6e05f6a1e2359", - "sources": { - "s3c9be24424e0": "https://raw.githubusercontent.com/simonw/datasette/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py", - "sffebbc2c5528": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L576-L655", - "seae12396d87a": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L658-L736", - "s9e735f75bf4b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L173-L179", - "s224af1264bb1": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L203-L306", - "sf0ea47721a46": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L352-L358", - "sae5997e91b6b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/cloudrun.py#L17-L244", - "s735b0ff17b61": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/heroku.py#L21-L254", - "s2484732fbf31": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/tests/test_package.py#L11-L61" - } - }, - "ds-cloud": { - "name": "Datasette Cloud Run publisher", - "basis": "e889403d3bbe143854262682161c98a57bdb6594", - "sources": { - "sf5859249ea9f": "https://github.com/simonw/datasette/blob/e889403d3bbe143854262682161c98a57bdb6594/datasette/publish/cloudrun.py" - } - }, - "fly": { - "name": "Datasette Fly publishing plugin", - "basis": "unknown", - "sources": { - "s2a2db4192225": "https://github.com/simonw/datasette-publish-fly" - } - }, - "passwords": { - "name": "datasette-auth-passwords", - "basis": "1.1.1", - "sources": { - "sb6597be5af67": "https://github.com/simonw/datasette-auth-passwords/blob/1.1.1/README.md" - } - }, - "github-auth": { - "name": "datasette-auth-github", - "basis": "unknown", - "sources": { - "s90107b903580": "https://github.com/simonw/datasette-auth-github" - } - }, - "write": { - "name": "datasette-write", - "basis": "0.4", - "sources": { - "s073e10d21091": "https://github.com/simonw/datasette-write/blob/0.4/README.md" - } - }, - "web": { - "name": "sqlite-web package", - "basis": "0.8.1", - "sources": { - "s91f8591a5f84": "https://github.com/coleifer/sqlite-web/blob/0.8.1/README.md" - } - }, - "web-source": { - "name": "sqlite-web implementation", - "basis": "825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2", - "sources": { - "s13d71c0a8cb2": "https://raw.githubusercontent.com/coleifer/sqlite-web/825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2/sqlite_web/sqlite_web.py" - } - }, - "web-tag-source": { - "name": "sqlite-web tagged implementation", - "basis": "0.8.1", - "sources": { - "s1626cd0d975f": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L3", - "s8c12f9aab57c": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1735-L1740", - "s30bba3ce63c6": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1944-L1956" - } - }, - "web-image": { - "name": "sqlite-web Dockerfile", - "basis": "7f6658b012a789f0b9418cad543e7798ce850130", - "sources": { - "s4c81e6becc20": "https://raw.githubusercontent.com/coleifer/sqlite-web/7f6658b012a789f0b9418cad543e7798ce850130/docker/Dockerfile" - } - }, - "sqlite": { - "name": "SQLite documentation", - "basis": "unknown", - "sources": { - "s4e4e320abf5b": "https://www.sqlite.org/uri.html", - "s2721da91f8d2": "https://www.sqlite.org/serverless.html", - "sadf0431c1271": "https://www.sqlite.org/security.html" - } - }, - "docker": { - "name": "Docker localhost-publication boundary", - "basis": "28.0.0", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - }, - "cloudrun": { - "name": "Cloud Run documentation", - "basis": "unknown", - "sources": { - "sb90a6c13388a": "https://docs.cloud.google.com/run/docs/container-contract" - } - }, - "nginx": { - "name": "nginx documentation", - "basis": "unknown", - "sources": { - "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", - "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html" - } - }, - "iap": { - "name": "Google IAP documentation", - "basis": "unknown", - "sources": { - "saafdfc163fce": "https://docs.cloud.google.com/iap/docs/concepts-overview" - } - }, - "access": { - "name": "Cloudflare Access documentation", - "basis": "unknown", - "sources": { - "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/", - "s06747cf7388c": "https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/independent-mfa/" - } - }, - "tailscale": { - "name": "Tailscale documentation", - "basis": "unknown", - "sources": { - "s5b9a9b15ae80": "https://tailscale.com/docs/reference/tailscale-cli/serve" - } - }, - "curl": { - "name": "curl minimum write-out version", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "ss": { - "name": "ss documentation", - "basis": "unknown", - "sources": { - "sfc58f455acfa": "https://man7.org/linux/man-pages/man8/ss.8.html" - } - }, - "datasette-docs": { - "name": "Datasette documentation (rolling)", - "basis": "unknown", - "sources": { - "s3e64933fe2e9": "https://docs.datasette.io/en/stable/changelog.html", - "sb0aae2446988": "https://docs.datasette.io/en/stable/cli-reference.html", - "sca59b58c5c84": "https://docs.datasette.io/en/stable/performance.html", - "s8f2fe7a13b3f": "https://docs.datasette.io/en/stable/installation.html", - "se7cc29f79be8": "https://docs.datasette.io/en/stable/publish.html", - "s878851fd8eac": "https://docs.datasette.io/en/stable/authentication.html", - "s824eb176fda7": "https://docs.datasette.io/en/stable/sql_queries.html", - "s406cc998a5da": "https://docs.datasette.io/en/stable/internals.html#csrf-protection", - "se154b0fa47dd": "https://docs.datasette.io/en/stable/settings.html", - "s96c8e6562139": "https://docs.datasette.io/en/stable/json_api.html", - "s1e93b280c5af": "https://docs.datasette.io/en/stable/introspection.html", - "sb1b30f98f026": "https://docs.datasette.io/en/stable/plugins.html#secret-configuration-values", - "s53e84082f6d1": "https://docs.datasette.io/en/stable/deploying.html" - } - }, - "caddy-docs": { - "name": "Caddy documentation (rolling)", - "basis": "unknown", - "sources": { - "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth" - } - } - }, - "claims": { - "release": {"text": "The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9"], "status": "REASONED"}, - "datasette-bind": {"text": "Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "DEMONSTRATED", "evidence": "bare `datasette serve` with no host or port flag listened on `127.0.0.1:8001`"}, - "immutable": {"text": "Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors.", "components": ["sqlite", "datasette-docs"], "sources": ["datasette-docs:sca59b58c5c84", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, - "datasette-docker": {"text": "Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate.", "components": ["datasette-docs"], "sources": ["datasette-docs:s8f2fe7a13b3f"], "status": "REASONED"}, - "container-generator": {"text": "At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address.", "components": ["ds-container"], "sources": ["ds-container:s3c9be24424e0", "ds-container:sffebbc2c5528", "ds-container:seae12396d87a", "ds-container:s9e735f75bf4b", "ds-container:s224af1264bb1", "ds-container:sf0ea47721a46", "ds-container:sae5997e91b6b", "ds-container:s735b0ff17b61", "ds-container:s2484732fbf31"], "status": "REASONED"}, - "cloudrun": {"text": "Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization.", "components": ["ds-cloud", "cloudrun"], "sources": ["ds-cloud:sf5859249ea9f", "cloudrun:sb90a6c13388a"], "status": "REASONED"}, - "publish-plugins": {"text": "Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established.", "components": ["fly", "datasette-docs"], "sources": ["datasette-docs:se7cc29f79be8", "fly:s2a2db4192225"], "status": "REASONED"}, - "docker-publication": {"text": "Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally.", "components": ["docker"], "sources": ["docker:s1e53417c513d"], "status": "REASONED"}, - "actor-policy": {"text": "Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it.", "components": ["github-auth", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "github-auth:s90107b903580"], "status": "REASONED"}, - "root-url": {"text": "datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:sb0aae2446988"], "status": "REASONED"}, - "canned-writes": {"text": "Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode.", "components": ["datasette-docs"], "sources": ["datasette-docs:s824eb176fda7"], "status": "REASONED"}, - "write-plugin": {"text": "datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "REASONED"}, - "sql-policy": {"text": "?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, - "query-limits": {"text": "Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, - "downloads": {"text": "allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd"], "status": "DEMONSTRATED", "evidence": "`/published.db` returned `403`; without `--setting allow_download off` it returned `200`."}, - "cors": {"text": "The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "DEMONSTRATED", "evidence": "`--cors` added `Access-Control-Allow-Origin: *`, absent without it."}, - "introspection": {"text": "/-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission.", "components": ["datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, - "hash-urls": {"text": "Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9", "datasette-docs:sca59b58c5c84"], "status": "REASONED"}, - "csrf": {"text": "Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads.", "components": ["datasette-docs"], "sources": ["datasette-docs:s406cc998a5da"], "status": "REASONED"}, - "plugin-secrets": {"text": "The local analyst saw inline password hashes in metadata, but only $env references after environment indirection.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "`/-/metadata.json` showed that analyst both users' password hashes when they were written inline in the plugin configuration, but only the `$env` references when they were supplied through environment variables."}, - "web-bind": {"text": "Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "bare `sqlite_web` on `127.0.0.1:8080`, as `ss -tlnp` run without `sudo` by the same account showed"}, - "web-capabilities": {"text": "sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, - "web-container": {"text": "sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy.", "components": ["web-image", "web"], "sources": ["web-image:s4c81e6becc20", "web:s91f8591a5f84"], "status": "REASONED"}, - "web-password": {"text": "Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "`SQLITE_WEB_PASSWORD` set without `-P` left sqlite-web open (`200` anonymously), as the source reading above says; with `-P` the anonymous query GET got `302` to `/login/`."}, - "web-readonly": {"text": "In the local --read-only run the anonymous read still returned 200 while the insert changed nothing.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "DEMONSTRATED", "evidence": "with `--read-only` the read still returned `200` and the insert changed nothing."}, - "web-methods": {"text": "Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access.", "components": ["web-source"], "sources": ["web-source:s13d71c0a8cb2"], "status": "REASONED"}, - "web-tls": {"text": "sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, - "web-csrf": {"text": "sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "an authenticated `POST` insert with no token and a foreign `Origin` header wrote a row, and the insert form contains no CSRF field."}, - "web-cookie": {"text": "sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Its session cookie was set with `HttpOnly; Path=/` and no `SameSite` or `Secure` attribute, even over TLS"}, - "fronting": {"text": "Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route.", "components": ["nginx", "caddy-docs", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "nginx:s6a0728ca7609", "nginx:sf32ce2956917", "caddy-docs:sbc2bd9599bd1"], "status": "REASONED"}, - "identity-proxy": {"text": "IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes.", "components": ["iap", "access"], "sources": ["iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED"}, - "mfa": {"text": "Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA.", "components": ["web", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "access:s06747cf7388c"], "status": "REASONED"}, - "tailnet": {"text": "Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured.", "components": ["tailscale"], "sources": ["tailscale:s5b9a9b15ae80"], "status": "REASONED"}, - "origin-bypass": {"text": "Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions.", "components": ["iap"], "sources": ["iap:saafdfc163fce"], "status": "REASONED"}, - "os-account": {"text": "Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority.", "components": ["sqlite"], "sources": ["sqlite:s2721da91f8d2", "sqlite:sadf0431c1271"], "status": "REASONED"}, - "verify-inventory": {"text": "Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability.", "components": ["web", "ss", "docker", "cloudrun", "datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "web:s91f8591a5f84", "ss:sfc58f455acfa", "docker:s1e53417c513d", "cloudrun:sb90a6c13388a"], "status": "REASONED", "verify": [1]}, - "verify-external": {"text": "External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available.", "components": ["web", "iap", "curl", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "web:s91f8591a5f84", "iap:saafdfc163fce", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, - "verify-anonymous": {"text": "UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control.", "components": ["web", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:s824eb176fda7", "web:s91f8591a5f84"], "status": "REASONED", "verify": [2]}, - "datasette-exposed": {"text": "Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "datasette-docs:se154b0fa47dd", "datasette-docs:s96c8e6562139", "datasette-docs:s1e93b280c5af"], "status": "DEMONSTRATED", "evidence": "Against the baseline command above, the SQL JSON route returned `403` while the table's JSON page still returned `200` (SQL disabled, browsing permitted)"}, - "web-exposed": {"text": "Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Bare `sqlite_web` answered the query GET with `200` anonymously, and an anonymous `POST` insert wrote a row to a disposable database"}, - "verify-authorized": {"text": "Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy.", "components": ["web", "curl", "iap", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "curl:s2b2686afaf41", "iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED", "verify": [3]}, - "actor-results": {"text": "Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "the block returned the table's rows to the `analyst` session cookie (`authorized http=200`) and `403` anonymously; an authenticated `guest` got `403`, the unauthorized outcome, and the analyst got `403` on SQL under `allow_sql: false`."}, - "web-auth-results": {"text": "Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "For sqlite-web with `-P`, the session cookie got the row with `200` and the anonymous request `302` to `/login/`."}, - "introspection-results": {"text": "Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "Under the same metadata, `/-/versions.json`, `/-/metadata.json`, `/-/plugins.json`, `/-/settings.json` and `/-/databases.json` returned `403` anonymously and `200` to a freshly logged-in analyst"}, - "caddy-results": {"text": "Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation.", "components": ["caddy-docs", "datasette-docs"], "sources": ["caddy-docs:sbc2bd9599bd1", "datasette-docs:s53e84082f6d1"], "status": "DEMONSTRATED", "evidence": "Block C through the proxy gave `authorized http=200` with the rows and `anonymous http=401` with `www-authenticate: Basic realm=\"restricted\"`."}, - "local-checks": {"text": "Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Local authoring checks passed: `bash -n` on all five shell blocks, parsing of the metadata JSON, and 120 guard executions covering 40 cases under ordinary Bash, `set -u`, and `set -u` with `IFS=0`."}, - "guard-limits": {"text": "Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Across 48 block B/C cases under ordinary Bash, set -u, and set -u with IFS=0, an explicitly substituted curl argument recorder ran with valid whole-block values, inherited matching marker/arguments, and pastes below the guards. Whole-block placeholders and omitted assignments without a matching marker exited 2 without calling it."}, - "datasette-cli": {"text": "Bare datasette invokes serve; -h/--host and -p/--port override its bind and port.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "REASONED"}, - "web-cli": {"text": "sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "REASONED"}, - "write-test": {"text": "Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "DEMONSTRATED", "evidence": "With datasette-write on a disposable database, an anonymous write got `403`, a root write without the CSRF token `403`, and with the token `302`, and the row was written."}, - "secret-indirection": {"text": "Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, - "web-auth-config": {"text": "In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles.", "components": ["web", "web-source", "web-tag-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "web-tag-source:s1626cd0d975f", "web-tag-source:s8c12f9aab57c", "web-tag-source:s30bba3ce63c6"], "status": "REASONED"}, - "web-readonly-config": {"text": "--read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, - "cors-policy": {"text": "--cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "REASONED"}, - "query-pages": {"text": "Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"} - } -} ---- # SQLite HTTP front-ends: Datasette and sqlite-web - -**Version basis** +Written against Datasette container generator (pinned to a commit), sqlite-web package 0.8.1, sqlite-web tagged implementation 0.8.1, datasette-auth-passwords 1.1.1, datasette-write 0.4, Docker localhost-publication boundary 28.0.0, and curl minimum write-out version 7.75.0; 3 further sources are pinned to a commit and 11 carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 55 claims: 18 demonstrated by a check that ran and 37 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. +An SQLite HTTP front-end turns a local file into a network service: **the port is the exposure**. Datasette allows anonymous browsing and read-only SQL by default; sqlite-web provides queries, imports, exports, row changes, and schema deletion. An unprotected, reachable instance exposes those capabilities. See Datasette permissions and sqlite-web features. -Components (recorded basis): - -- ds-container: Datasette container generator (caf238aac86ebe370959b25384d6e05f6a1e2359) -- ds-cloud: Datasette Cloud Run publisher (e889403d3bbe143854262682161c98a57bdb6594) -- fly: Datasette Fly publishing plugin (unknown) -- passwords: datasette-auth-passwords (1.1.1) -- github-auth: datasette-auth-github (unknown) -- write: datasette-write (0.4) -- web: sqlite-web package (0.8.1) -- web-source: sqlite-web implementation (825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2) -- web-tag-source: sqlite-web tagged implementation (0.8.1) -- web-image: sqlite-web Dockerfile (7f6658b012a789f0b9418cad543e7798ce850130) -- sqlite: SQLite documentation (unknown) -- docker: Docker localhost-publication boundary (28.0.0) -- cloudrun: Cloud Run documentation (unknown) -- nginx: nginx documentation (unknown) -- iap: Google IAP documentation (unknown) -- access: Cloudflare Access documentation (unknown) -- tailscale: Tailscale documentation (unknown) -- curl: curl minimum write-out version (7.75.0) -- ss: ss documentation (unknown) -- datasette-docs: Datasette documentation (rolling) (unknown) -- caddy-docs: Caddy documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): - -- release (REASONED; datasette-docs): The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes. -- datasette-bind (DEMONSTRATED; datasette-docs): Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory. -- immutable (REASONED; sqlite, datasette-docs): Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors. -- datasette-docker (REASONED; datasette-docs): Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate. -- container-generator (REASONED; ds-container): At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address. -- cloudrun (REASONED; ds-cloud, cloudrun): Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization. -- publish-plugins (REASONED; fly, datasette-docs): Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established. -- docker-publication (REASONED; docker): Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally. -- actor-policy (REASONED; github-auth, datasette-docs): Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it. -- root-url (REASONED; datasette-docs): datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential. -- canned-writes (REASONED; datasette-docs): Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode. -- write-plugin (REASONED; write, datasette-docs): datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes. -- sql-policy (REASONED; datasette-docs): ?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists. -- query-limits (REASONED; datasette-docs): Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service. -- downloads (DEMONSTRATED; datasette-docs): allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403. -- cors (DEMONSTRATED; datasette-docs): The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag. -- introspection (REASONED; datasette-docs): /-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission. -- hash-urls (REASONED; datasette-docs): Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization. -- csrf (REASONED; datasette-docs): Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads. -- plugin-secrets (DEMONSTRATED; passwords, datasette-docs): The local analyst saw inline password hashes in metadata, but only $env references after environment indirection. -- web-bind (DEMONSTRATED; web, web-source): Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory. -- web-capabilities (REASONED; web): sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise. -- web-container (REASONED; web-image, web): sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy. -- web-password (DEMONSTRATED; web, web-source): Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/. -- web-readonly (DEMONSTRATED; web, web-source, sqlite): In the local --read-only run the anonymous read still returned 200 while the insert changed nothing. -- web-methods (REASONED; web-source): Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access. -- web-tls (REASONED; web): sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy. -- web-csrf (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy. -- web-cookie (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults. -- fronting (REASONED; nginx, caddy-docs, datasette-docs): Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route. -- identity-proxy (REASONED; iap, access): IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes. -- mfa (REASONED; web, access, datasette-docs): Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA. -- tailnet (REASONED; tailscale): Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured. -- origin-bypass (REASONED; iap): Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions. -- os-account (REASONED; sqlite): Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority. -- verify-inventory (REASONED; web, ss, docker, cloudrun, datasette-docs): Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability. -- verify-external (REASONED; web, iap, curl, datasette-docs): External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available. -- verify-anonymous (REASONED; web, datasette-docs): UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control. -- datasette-exposed (DEMONSTRATED; datasette-docs): Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200. -- web-exposed (DEMONSTRATED; web, web-source): Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database. -- verify-authorized (REASONED; web, curl, iap, access, datasette-docs): Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy. -- actor-results (DEMONSTRATED; passwords, datasette-docs): Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false. -- web-auth-results (DEMONSTRATED; web, web-source): Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/. -- introspection-results (DEMONSTRATED; passwords, datasette-docs): Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst. -- caddy-results (DEMONSTRATED; caddy-docs, datasette-docs): Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation. -- local-checks (DEMONSTRATED; curl): Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes. -- guard-limits (DEMONSTRATED; curl): Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage. -- datasette-cli (REASONED; datasette-docs): Bare datasette invokes serve; -h/--host and -p/--port override its bind and port. -- web-cli (REASONED; web, web-source): sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080. -- write-test (DEMONSTRATED; write, datasette-docs): Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row. -- secret-indirection (REASONED; datasette-docs): Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files. -- web-auth-config (REASONED; web, web-source, web-tag-source): In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles. -- web-readonly-config (REASONED; web, web-source, sqlite): --read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers. -- cors-policy (REASONED; datasette-docs): --cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests. -- query-pages (REASONED; datasette-docs): Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary. - - -An SQLite HTTP front-end turns a local file into a network service: **the port is the exposure**. Datasette allows anonymous browsing and read-only SQL by default; sqlite-web provides queries, imports, exports, row changes, and schema deletion. An unprotected, reachable instance exposes those capabilities. See Datasette permissions and sqlite-web features. - -A reachable, unauthenticated, writable sqlite-web instance therefore permits compromise of the databases its process can modify. That is an inference from its documented capabilities, not a claim of automatic host compromise. +A reachable, unauthenticated, writable sqlite-web instance therefore permits compromise of the databases its process can modify. That is an inference from its documented capabilities, not a claim of automatic host compromise. Use [sqlite.md](sqlite.md) for the files at rest and [web-exposure.md](web-exposure.md) for accidental file publication. Application-level SQL injection is outside this guide's scope; see the OWASP SQL Injection Prevention Cheat Sheet. @@ -604,3 +278,355 @@ Every URL listed below was fetched during authoring, except the sqlite-web 0.8.1 - curl manual (write-out variables `exitcode` and `errormsg` added in curl 7.75.0): https://curl.se/docs/manpage.html - ss manual: https://man7.org/linux/man-pages/man8/ss.8.html - OWASP SQL injection prevention: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.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/sqlite-http-frontends.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): + +- ds-container: Datasette container generator (caf238aac86ebe370959b25384d6e05f6a1e2359) +- web: sqlite-web package (0.8.1) +- web-tag-source: sqlite-web tagged implementation (0.8.1) +- ds-cloud: Datasette Cloud Run publisher (e889403d3bbe143854262682161c98a57bdb6594) +- fly: Datasette Fly publishing plugin (unknown) +- datasette-docs: Datasette documentation (rolling) (unknown) +- web-source: sqlite-web implementation (825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2) +- web-image: sqlite-web Dockerfile (7f6658b012a789f0b9418cad543e7798ce850130) +- passwords: datasette-auth-passwords (1.1.1) +- github-auth: datasette-auth-github (unknown) +- write: datasette-write (0.4) +- sqlite: SQLite documentation (unknown) +- docker: Docker localhost-publication boundary (28.0.0) +- cloudrun: Cloud Run documentation (unknown) +- nginx: nginx documentation (unknown) +- iap: Google IAP documentation (unknown) +- access: Cloudflare Access documentation (unknown) +- tailscale: Tailscale documentation (unknown) +- caddy-docs: Caddy documentation (rolling) (unknown) +- curl: curl minimum write-out version (7.75.0) +- ss: ss documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- release (REASONED; datasette-docs): The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes. +- datasette-bind (DEMONSTRATED; datasette-docs): Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory. +- immutable (REASONED; sqlite, datasette-docs): Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors. +- datasette-docker (REASONED; datasette-docs): Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate. +- container-generator (REASONED; ds-container): At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address. +- cloudrun (REASONED; ds-cloud, cloudrun): Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization. +- publish-plugins (REASONED; fly, datasette-docs): Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established. +- docker-publication (REASONED; docker): Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally. +- actor-policy (REASONED; github-auth, datasette-docs): Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it. +- root-url (REASONED; datasette-docs): datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential. +- canned-writes (REASONED; datasette-docs): Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode. +- write-plugin (REASONED; write, datasette-docs): datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes. +- sql-policy (REASONED; datasette-docs): ?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists. +- query-limits (REASONED; datasette-docs): Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service. +- downloads (DEMONSTRATED; datasette-docs): allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403. +- cors (DEMONSTRATED; datasette-docs): The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag. +- introspection (REASONED; datasette-docs): /-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission. +- hash-urls (REASONED; datasette-docs): Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization. +- csrf (REASONED; datasette-docs): Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads. +- plugin-secrets (DEMONSTRATED; passwords, datasette-docs): The local analyst saw inline password hashes in metadata, but only $env references after environment indirection. +- web-bind (DEMONSTRATED; web, web-source): Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory. +- web-capabilities (REASONED; web): sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise. +- web-container (REASONED; web-image, web): sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy. +- web-password (DEMONSTRATED; web, web-source): Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/. +- web-readonly (DEMONSTRATED; web, web-source, sqlite): In the local --read-only run the anonymous read still returned 200 while the insert changed nothing. +- web-methods (REASONED; web-source): Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access. +- web-tls (REASONED; web): sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy. +- web-csrf (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy. +- web-cookie (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults. +- fronting (REASONED; nginx, caddy-docs, datasette-docs): Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route. +- identity-proxy (REASONED; iap, access): IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes. +- mfa (REASONED; web, access, datasette-docs): Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA. +- tailnet (REASONED; tailscale): Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured. +- origin-bypass (REASONED; iap): Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions. +- os-account (REASONED; sqlite): Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority. +- verify-inventory (REASONED; web, ss, docker, cloudrun, datasette-docs): Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability. +- verify-external (REASONED; web, iap, curl, datasette-docs): External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available. +- verify-anonymous (REASONED; web, datasette-docs): UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control. +- datasette-exposed (DEMONSTRATED; datasette-docs): Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200. +- web-exposed (DEMONSTRATED; web, web-source): Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database. +- verify-authorized (REASONED; web, curl, iap, access, datasette-docs): Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy. +- actor-results (DEMONSTRATED; passwords, datasette-docs): Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false. +- web-auth-results (DEMONSTRATED; web, web-source): Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/. +- introspection-results (DEMONSTRATED; passwords, datasette-docs): Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst. +- caddy-results (DEMONSTRATED; caddy-docs, datasette-docs): Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation. +- local-checks (DEMONSTRATED; curl): Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes. +- guard-limits (DEMONSTRATED; curl): Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage. +- datasette-cli (REASONED; datasette-docs): Bare datasette invokes serve; -h/--host and -p/--port override its bind and port. +- web-cli (REASONED; web, web-source): sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080. +- write-test (DEMONSTRATED; write, datasette-docs): Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row. +- secret-indirection (REASONED; datasette-docs): Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files. +- web-auth-config (REASONED; web, web-source, web-tag-source): In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles. +- web-readonly-config (REASONED; web, web-source, sqlite): --read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers. +- cors-policy (REASONED; datasette-docs): --cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests. +- query-pages (REASONED; datasette-docs): Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "cd2fba66ff332b791a1934a25ece6c5ef7f76fc791a8a4019819189f360eab0b", + "components": { + "ds-container": { + "name": "Datasette container generator", + "basis": "caf238aac86ebe370959b25384d6e05f6a1e2359", + "sources": { + "s3c9be24424e0": "https://raw.githubusercontent.com/simonw/datasette/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py", + "sffebbc2c5528": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L576-L655", + "seae12396d87a": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L658-L736", + "s9e735f75bf4b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L173-L179", + "s224af1264bb1": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L203-L306", + "sf0ea47721a46": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L352-L358", + "sae5997e91b6b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/cloudrun.py#L17-L244", + "s735b0ff17b61": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/heroku.py#L21-L254", + "s2484732fbf31": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/tests/test_package.py#L11-L61" + } + }, + "web": { + "name": "sqlite-web package", + "basis": "0.8.1", + "sources": { + "s91f8591a5f84": "https://github.com/coleifer/sqlite-web/blob/0.8.1/README.md" + } + }, + "web-tag-source": { + "name": "sqlite-web tagged implementation", + "basis": "0.8.1", + "sources": { + "s1626cd0d975f": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L3", + "s8c12f9aab57c": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1735-L1740", + "s30bba3ce63c6": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1944-L1956" + } + }, + "ds-cloud": { + "name": "Datasette Cloud Run publisher", + "basis": "e889403d3bbe143854262682161c98a57bdb6594", + "sources": { + "sf5859249ea9f": "https://github.com/simonw/datasette/blob/e889403d3bbe143854262682161c98a57bdb6594/datasette/publish/cloudrun.py" + } + }, + "fly": { + "name": "Datasette Fly publishing plugin", + "basis": "unknown", + "sources": { + "s2a2db4192225": "https://github.com/simonw/datasette-publish-fly" + } + }, + "datasette-docs": { + "name": "Datasette documentation (rolling)", + "basis": "unknown", + "sources": { + "s3e64933fe2e9": "https://docs.datasette.io/en/stable/changelog.html", + "sb0aae2446988": "https://docs.datasette.io/en/stable/cli-reference.html", + "sca59b58c5c84": "https://docs.datasette.io/en/stable/performance.html", + "s8f2fe7a13b3f": "https://docs.datasette.io/en/stable/installation.html", + "se7cc29f79be8": "https://docs.datasette.io/en/stable/publish.html", + "s878851fd8eac": "https://docs.datasette.io/en/stable/authentication.html", + "s824eb176fda7": "https://docs.datasette.io/en/stable/sql_queries.html", + "s406cc998a5da": "https://docs.datasette.io/en/stable/internals.html#csrf-protection", + "se154b0fa47dd": "https://docs.datasette.io/en/stable/settings.html", + "s96c8e6562139": "https://docs.datasette.io/en/stable/json_api.html", + "s1e93b280c5af": "https://docs.datasette.io/en/stable/introspection.html", + "sb1b30f98f026": "https://docs.datasette.io/en/stable/plugins.html#secret-configuration-values", + "s53e84082f6d1": "https://docs.datasette.io/en/stable/deploying.html" + } + }, + "web-source": { + "name": "sqlite-web implementation", + "basis": "825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2", + "sources": { + "s13d71c0a8cb2": "https://raw.githubusercontent.com/coleifer/sqlite-web/825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2/sqlite_web/sqlite_web.py" + } + }, + "web-image": { + "name": "sqlite-web Dockerfile", + "basis": "7f6658b012a789f0b9418cad543e7798ce850130", + "sources": { + "s4c81e6becc20": "https://raw.githubusercontent.com/coleifer/sqlite-web/7f6658b012a789f0b9418cad543e7798ce850130/docker/Dockerfile" + } + }, + "passwords": { + "name": "datasette-auth-passwords", + "basis": "1.1.1", + "sources": { + "sb6597be5af67": "https://github.com/simonw/datasette-auth-passwords/blob/1.1.1/README.md" + } + }, + "github-auth": { + "name": "datasette-auth-github", + "basis": "unknown", + "sources": { + "s90107b903580": "https://github.com/simonw/datasette-auth-github" + } + }, + "write": { + "name": "datasette-write", + "basis": "0.4", + "sources": { + "s073e10d21091": "https://github.com/simonw/datasette-write/blob/0.4/README.md" + } + }, + "sqlite": { + "name": "SQLite documentation", + "basis": "unknown", + "sources": { + "s4e4e320abf5b": "https://www.sqlite.org/uri.html", + "s2721da91f8d2": "https://www.sqlite.org/serverless.html", + "sadf0431c1271": "https://www.sqlite.org/security.html" + } + }, + "docker": { + "name": "Docker localhost-publication boundary", + "basis": "28.0.0", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "cloudrun": { + "name": "Cloud Run documentation", + "basis": "unknown", + "sources": { + "sb90a6c13388a": "https://docs.cloud.google.com/run/docs/container-contract" + } + }, + "nginx": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", + "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html" + } + }, + "iap": { + "name": "Google IAP documentation", + "basis": "unknown", + "sources": { + "saafdfc163fce": "https://docs.cloud.google.com/iap/docs/concepts-overview" + } + }, + "access": { + "name": "Cloudflare Access documentation", + "basis": "unknown", + "sources": { + "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/", + "s06747cf7388c": "https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/independent-mfa/" + } + }, + "tailscale": { + "name": "Tailscale documentation", + "basis": "unknown", + "sources": { + "s5b9a9b15ae80": "https://tailscale.com/docs/reference/tailscale-cli/serve" + } + }, + "caddy-docs": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "ss": { + "name": "ss documentation", + "basis": "unknown", + "sources": { + "sfc58f455acfa": "https://man7.org/linux/man-pages/man8/ss.8.html" + } + } + }, + "claims": { + "release": {"text": "The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9"], "status": "REASONED"}, + "datasette-bind": {"text": "Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "DEMONSTRATED", "evidence": "bare `datasette serve` with no host or port flag listened on `127.0.0.1:8001`"}, + "immutable": {"text": "Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors.", "components": ["sqlite", "datasette-docs"], "sources": ["datasette-docs:sca59b58c5c84", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, + "datasette-docker": {"text": "Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate.", "components": ["datasette-docs"], "sources": ["datasette-docs:s8f2fe7a13b3f"], "status": "REASONED"}, + "container-generator": {"text": "At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address.", "components": ["ds-container"], "sources": ["ds-container:s3c9be24424e0", "ds-container:sffebbc2c5528", "ds-container:seae12396d87a", "ds-container:s9e735f75bf4b", "ds-container:s224af1264bb1", "ds-container:sf0ea47721a46", "ds-container:sae5997e91b6b", "ds-container:s735b0ff17b61", "ds-container:s2484732fbf31"], "status": "REASONED"}, + "cloudrun": {"text": "Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization.", "components": ["ds-cloud", "cloudrun"], "sources": ["ds-cloud:sf5859249ea9f", "cloudrun:sb90a6c13388a"], "status": "REASONED"}, + "publish-plugins": {"text": "Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established.", "components": ["fly", "datasette-docs"], "sources": ["datasette-docs:se7cc29f79be8", "fly:s2a2db4192225"], "status": "REASONED"}, + "docker-publication": {"text": "Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally.", "components": ["docker"], "sources": ["docker:s1e53417c513d"], "status": "REASONED"}, + "actor-policy": {"text": "Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it.", "components": ["github-auth", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "github-auth:s90107b903580"], "status": "REASONED"}, + "root-url": {"text": "datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:sb0aae2446988"], "status": "REASONED"}, + "canned-writes": {"text": "Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode.", "components": ["datasette-docs"], "sources": ["datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "write-plugin": {"text": "datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "REASONED"}, + "sql-policy": {"text": "?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "query-limits": {"text": "Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "downloads": {"text": "allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd"], "status": "DEMONSTRATED", "evidence": "`/published.db` returned `403`; without `--setting allow_download off` it returned `200`."}, + "cors": {"text": "The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "DEMONSTRATED", "evidence": "`--cors` added `Access-Control-Allow-Origin: *`, absent without it."}, + "introspection": {"text": "/-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission.", "components": ["datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, + "hash-urls": {"text": "Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9", "datasette-docs:sca59b58c5c84"], "status": "REASONED"}, + "csrf": {"text": "Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads.", "components": ["datasette-docs"], "sources": ["datasette-docs:s406cc998a5da"], "status": "REASONED"}, + "plugin-secrets": {"text": "The local analyst saw inline password hashes in metadata, but only $env references after environment indirection.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "`/-/metadata.json` showed that analyst both users' password hashes when they were written inline in the plugin configuration, but only the `$env` references when they were supplied through environment variables."}, + "web-bind": {"text": "Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "bare `sqlite_web` on `127.0.0.1:8080`, as `ss -tlnp` run without `sudo` by the same account showed"}, + "web-capabilities": {"text": "sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, + "web-container": {"text": "sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy.", "components": ["web-image", "web"], "sources": ["web-image:s4c81e6becc20", "web:s91f8591a5f84"], "status": "REASONED"}, + "web-password": {"text": "Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "`SQLITE_WEB_PASSWORD` set without `-P` left sqlite-web open (`200` anonymously), as the source reading above says; with `-P` the anonymous query GET got `302` to `/login/`."}, + "web-readonly": {"text": "In the local --read-only run the anonymous read still returned 200 while the insert changed nothing.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "DEMONSTRATED", "evidence": "with `--read-only` the read still returned `200` and the insert changed nothing."}, + "web-methods": {"text": "Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access.", "components": ["web-source"], "sources": ["web-source:s13d71c0a8cb2"], "status": "REASONED"}, + "web-tls": {"text": "sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, + "web-csrf": {"text": "sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "an authenticated `POST` insert with no token and a foreign `Origin` header wrote a row, and the insert form contains no CSRF field."}, + "web-cookie": {"text": "sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Its session cookie was set with `HttpOnly; Path=/` and no `SameSite` or `Secure` attribute, even over TLS"}, + "fronting": {"text": "Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route.", "components": ["nginx", "caddy-docs", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "nginx:s6a0728ca7609", "nginx:sf32ce2956917", "caddy-docs:sbc2bd9599bd1"], "status": "REASONED"}, + "identity-proxy": {"text": "IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes.", "components": ["iap", "access"], "sources": ["iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED"}, + "mfa": {"text": "Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA.", "components": ["web", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "access:s06747cf7388c"], "status": "REASONED"}, + "tailnet": {"text": "Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured.", "components": ["tailscale"], "sources": ["tailscale:s5b9a9b15ae80"], "status": "REASONED"}, + "origin-bypass": {"text": "Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions.", "components": ["iap"], "sources": ["iap:saafdfc163fce"], "status": "REASONED"}, + "os-account": {"text": "Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority.", "components": ["sqlite"], "sources": ["sqlite:s2721da91f8d2", "sqlite:sadf0431c1271"], "status": "REASONED"}, + "verify-inventory": {"text": "Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability.", "components": ["web", "ss", "docker", "cloudrun", "datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "web:s91f8591a5f84", "ss:sfc58f455acfa", "docker:s1e53417c513d", "cloudrun:sb90a6c13388a"], "status": "REASONED", "verify": [1]}, + "verify-external": {"text": "External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available.", "components": ["web", "iap", "curl", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "web:s91f8591a5f84", "iap:saafdfc163fce", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, + "verify-anonymous": {"text": "UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control.", "components": ["web", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:s824eb176fda7", "web:s91f8591a5f84"], "status": "REASONED", "verify": [2]}, + "datasette-exposed": {"text": "Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "datasette-docs:se154b0fa47dd", "datasette-docs:s96c8e6562139", "datasette-docs:s1e93b280c5af"], "status": "DEMONSTRATED", "evidence": "Against the baseline command above, the SQL JSON route returned `403` while the table's JSON page still returned `200` (SQL disabled, browsing permitted)"}, + "web-exposed": {"text": "Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Bare `sqlite_web` answered the query GET with `200` anonymously, and an anonymous `POST` insert wrote a row to a disposable database"}, + "verify-authorized": {"text": "Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy.", "components": ["web", "curl", "iap", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "curl:s2b2686afaf41", "iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED", "verify": [3]}, + "actor-results": {"text": "Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "the block returned the table's rows to the `analyst` session cookie (`authorized http=200`) and `403` anonymously; an authenticated `guest` got `403`, the unauthorized outcome, and the analyst got `403` on SQL under `allow_sql: false`."}, + "web-auth-results": {"text": "Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "For sqlite-web with `-P`, the session cookie got the row with `200` and the anonymous request `302` to `/login/`."}, + "introspection-results": {"text": "Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "Under the same metadata, `/-/versions.json`, `/-/metadata.json`, `/-/plugins.json`, `/-/settings.json` and `/-/databases.json` returned `403` anonymously and `200` to a freshly logged-in analyst"}, + "caddy-results": {"text": "Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation.", "components": ["caddy-docs", "datasette-docs"], "sources": ["caddy-docs:sbc2bd9599bd1", "datasette-docs:s53e84082f6d1"], "status": "DEMONSTRATED", "evidence": "Block C through the proxy gave `authorized http=200` with the rows and `anonymous http=401` with `www-authenticate: Basic realm=\"restricted\"`."}, + "local-checks": {"text": "Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Local authoring checks passed: `bash -n` on all five shell blocks, parsing of the metadata JSON, and 120 guard executions covering 40 cases under ordinary Bash, `set -u`, and `set -u` with `IFS=0`."}, + "guard-limits": {"text": "Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Across 48 block B/C cases under ordinary Bash, set -u, and set -u with IFS=0, an explicitly substituted curl argument recorder ran with valid whole-block values, inherited matching marker/arguments, and pastes below the guards. Whole-block placeholders and omitted assignments without a matching marker exited 2 without calling it."}, + "datasette-cli": {"text": "Bare datasette invokes serve; -h/--host and -p/--port override its bind and port.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "REASONED"}, + "web-cli": {"text": "sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "REASONED"}, + "write-test": {"text": "Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "DEMONSTRATED", "evidence": "With datasette-write on a disposable database, an anonymous write got `403`, a root write without the CSRF token `403`, and with the token `302`, and the row was written."}, + "secret-indirection": {"text": "Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, + "web-auth-config": {"text": "In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles.", "components": ["web", "web-source", "web-tag-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "web-tag-source:s1626cd0d975f", "web-tag-source:s8c12f9aab57c", "web-tag-source:s30bba3ce63c6"], "status": "REASONED"}, + "web-readonly-config": {"text": "--read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, + "cors-policy": {"text": "--cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "REASONED"}, + "query-pages": {"text": "Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/sqlite.md b/plugin/skills/secure-deployment-config/references/sqlite.md index ed5228c44..dae404d00 100644 --- a/plugin/skills/secure-deployment-config/references/sqlite.md +++ b/plugin/skills/secure-deployment-config/references/sqlite.md @@ -1,341 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-17", - "documentation_checked": "2026-09", - "body_sha256": "1df2b900bb4797e8f2561a2cb2066c6c74bf8505a9043279c660e1d96229198f", - "components": { - "see": { - "name": "SQLite Encryption Extension", - "basis": "unknown", - "sources": { - "sda3d8876503d": "https://www.sqlite.org/see/doc/trunk/www/readme.wiki" - } - }, - "cipher": { - "name": "SQLCipher", - "basis": "unknown", - "sources": { - "s57d83a42a74a": "https://www.zetetic.net/sqlcipher/" - } - }, - "turso": { - "name": "Turso documentation", - "basis": "unknown", - "sources": { - "sb32fd9c679e1": "https://docs.turso.tech/sdk/http/quickstart", - "sfec4e734f423": "https://docs.turso.tech/cli/db/tokens/create", - "se409838a474a": "https://docs.turso.tech/sdk/http/reference" - } - }, - "litestream": { - "name": "Litestream documentation", - "basis": "unknown", - "sources": { - "sec3d72af1447": "https://litestream.io/guides/", - "s87fa51accafc": "https://litestream.io/guides/s3/", - "sa5dddf04652c": "https://litestream.io/reference/config/" - } - }, - "mcp": { - "name": "Litestream MCP minimum", - "basis": "v0.5.0", - "sources": { - "sa5dddf04652c": "https://litestream.io/reference/config/" - } - }, - "litefs": { - "name": "LiteFS source", - "basis": "v0.5.14", - "sources": { - "s694a6ba8eafa": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L134-L268", - "s864cacb8e7cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L76-L99", - "s16da1761d6cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/client.go#L32-L43", - "sf0e982d0e36a": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L57", - "s9389d639da6b": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L54-L58", - "s475c28fd949e": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L76-L110", - "s055e15e84ba5": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L219-L233", - "s0b0bac964067": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L288-L333", - "s326a1007e5c2": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L294-L318", - "s4547cc32557d": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2779-L2811", - "s85ece0e83d43": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1904-L1910", - "sa467eeb47f34": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L320-L346", - "s5ffa5c20fc78": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2682-L2776", - "s02908966756d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L495-L520", - "sd63fa776fbc1": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L526-L585", - "s1c51b7efc42d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L686-L699", - "scd0168363c1a": "https://github.com/superfly/litefs/blob/v0.5.14/http/http.go#L15-L43", - "s298808a10d50": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L461-L493", - "s7269cca8b0ea": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2387-L2448", - "sbd59a1af784e": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2452-L2609", - "sa199174f541b": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L348-L407", - "s750403129022": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L215-L325", - "s5009045b97e1": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1525-L1531", - "se2add54cf935": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L39-L41", - "s59a2dfccd853": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1455-L1465", - "s246e6ad0d330": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L409-L459", - "sae4ece3fbc87": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L408-L432", - "s7bcb006d6884": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L168-L170", - "se1a41376a98d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L271-L292", - "s93c0ad91a567": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L779-L803", - "s36792457dd64": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L487-L488", - "s7234e34af9ef": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1661-L1713", - "s4cebe21e711d": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L329-L348", - "sb43f5d78723c": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L241-L244", - "s8dfa3f10f3d3": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L99-L131", - "s83d8bd834347": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1384-L1387", - "s3284d1a3ecad": "https://github.com/superfly/litefs/blob/v0.5.14/Dockerfile", - "s2b80fdf8adbe": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L519-L524", - "sc0568fe7e0ee": "https://github.com/superfly/litefs/blob/v0.5.14/http/proxy_server.go#L131-L144", - "sf0d31f900082": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L60-L68", - "s82103ee03d64": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L32-L35" - } - }, - "litefs-docs": { - "name": "LiteFS documentation", - "basis": "unknown", - "sources": { - "s13b9e92d9996": "https://fly.io/docs/litefs/", - "sd7c0ba136b25": "https://fly.io/docs/litefs/config/" - } - }, - "go": { - "name": "Go library documentation", - "basis": "unknown", - "sources": { - "s6c5be8454523": "https://pkg.go.dev/github.com/golang/net/http2/h2c#NewHandler", - "s37204ff1b27c": "https://pkg.go.dev/net#Listen", - "se49529035a0c": "https://pkg.go.dev/expvar", - "s0246034dddd1": "https://pkg.go.dev/net/http/pprof" - } - }, - "docker": { - "name": "Docker documentation", - "basis": "unknown", - "sources": { - "s881082f899b2": "https://docs.docker.com/engine/network/drivers/bridge/", - "s6732393fd414": "https://docs.docker.com/engine/network/drivers/host/", - "s00095bd02553": "https://docs.docker.com/engine/network/#container-networks" - } - }, - "sqlite-rolling": { - "name": "SQLite documentation (rolling)", - "basis": "unknown", - "sources": { - "s2721da91f8d2": "https://www.sqlite.org/serverless.html", - "sf3d7f8391994": "https://www.sqlite.org/tempfiles.html", - "s742deb3338bb": "https://sqlite.org/loadext.html", - "sadf0431c1271": "https://www.sqlite.org/security.html" - } - }, - "git": { - "name": "Git documentation", - "basis": "v2.56.0", - "sources": { - "s12aad6467212": "https://github.com/git/git/blob/v2.56.0/Documentation/gitignore.adoc#L15-L18", - "s8054ab09dc74": "https://github.com/git/git/blob/v2.56.0/Documentation/git-rm.adoc#L17-L28", - "s9a6802adec89": "https://github.com/git/git/blob/v2.56.0/Documentation/git-log.adoc#L16-L16" - } - }, - "coreutils": { - "name": "GNU Coreutils stat manual", - "basis": "v9.7", - "sources": { - "s247df4545dc0": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L12989-L12995", - "sfd49553ab6e5": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13046", - "sbd6f7cad7bf4": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13062", - "s3e55a7242c33": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13073" - } - }, - "bash": { - "name": "GNU Bash reference manual", - "basis": "bash-5.3", - "sources": { - "s3711969be580": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n450", - "s7fec9b923488": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n769", - "s77b19e384d70": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1397", - "se4f37e01a249": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1866", - "s07f77f16f9f5": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2288", - "sbed8cd562a35": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2993", - "s7c9e44003867": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3131", - "s223b5c4d9a28": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4350", - "se02ece9163d4": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4582", - "s6067b2b1f78a": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5216", - "sa506f1abd6a1": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6182", - "s7d29125b4005": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n8232", - "s8962e63cfb20": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2580", - "s8a27e686ddfb": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5466", - "s213f6d8d1f07": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3280", - "sf9757e9fd6e8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3872", - "s8cbc1b4709e2": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4737", - "s3255eca7e496": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4852", - "s252aa2e81179": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5156", - "s8447036b2114": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5161", - "s5cab49305f3d": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5548", - "s6e8ae812c1c3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5625", - "sb38bf74a89b0": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5629", - "s260459961fbc": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5659", - "s8c59508bd4c8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5917", - "s8016d1aecfca": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5929", - "se62fd3ad4fc3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6122", - "s2df39d2c9c1c": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6194", - "s80e3246e96d8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n7274" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s5b71321b79ea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/disable.md#L4-L20", - "sc405a4bffc27": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/globoff.md#L4-L22", - "s345ec65025b0": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/noproxy.md#L18-L28", - "sc9973c4ceec1": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/connect-timeout.md#L19-L28", - "s7b6dca376156": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/max-time.md#L21-L27", - "s156481f6b726": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/silent.md#L4-L25", - "s07916104bf60": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/show-error.md#L4-L19", - "sa937dbe5dd4a": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L4-L33", - "s021603e35e68": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L49-L53", - "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", - "s39402f9cb25c": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L105-L107", - "sf2dcd83157a6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/insecure.md#L4-L30", - "s13f463154cea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/fail.md#L24-L28", - "s2f1ff2f6cacd": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/output.md#L4-L53", - "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49", - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" - } - }, - "grep-docs": { - "name": "GNU grep documentation", - "basis": "v3.12", - "sources": { - "s1733b640052b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197", - "s99f121f5426a": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342", - "sbc8d05ab659b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621", - "sfdd7a73eab39": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757", - "s7f9dc35e81c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788", - "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", - "sc08d6af4d642": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142", - "scd528c854b46": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150", - "sb8be8b716426": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274", - "s5d5af9297df2": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316", - "se99a83dba3c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363" - } - } - }, - "claims": { - "filesystem": {"text": "No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sadf0431c1271"], "status": "REASONED"}, - "extensions": {"text": "Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s742deb3338bb"], "status": "REASONED"}, - "web-files": {"text": "Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "git-files": {"text": "Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit.", "components": ["sqlite-rolling", "git"], "sources": ["sqlite-rolling:sf3d7f8391994", "git:s12aad6467212", "git:s8054ab09dc74", "git:s9a6802adec89"], "status": "REASONED"}, - "permissions": {"text": "Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "temporary": {"text": "Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "encryption": {"text": "Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption.", "components": ["see", "cipher"], "sources": ["see:sda3d8876503d", "cipher:s57d83a42a74a"], "status": "REASONED"}, - "encryption-keys": {"text": "Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak.", "components": ["see", "sqlite-rolling"], "sources": ["see:sda3d8876503d", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "turso-http": {"text": "Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git.", "components": ["turso"], "sources": ["turso:sb32fd9c679e1"], "status": "REASONED"}, - "turso-scope": {"text": "db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens.", "components": ["turso"], "sources": ["turso:sfec4e734f423"], "status": "REASONED"}, - "replicas": {"text": "Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials.", "components": ["litestream"], "sources": ["litestream:sec3d72af1447", "litestream:s87fa51accafc"], "status": "REASONED"}, - "metrics": {"text": "Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090.", "components": ["litestream"], "sources": ["litestream:sa5dddf04652c"], "status": "REASONED"}, - "mcp": {"text": "Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001.", "components": ["mcp"], "sources": ["mcp:sa5dddf04652c"], "status": "REASONED"}, - "litefs-backups": {"text": "LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations.", "components": ["litefs-docs"], "sources": ["litefs-docs:s13b9e92d9996"], "status": "REASONED"}, - "litefs-transport": {"text": "LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc", "litefs:s16da1761d6cc", "go:s6c5be8454523"], "status": "REASONED"}, - "litefs-bind": {"text": "http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port.", "components": ["litefs", "go"], "sources": ["litefs:sf0e982d0e36a", "litefs:s9389d639da6b", "go:s37204ff1b27c", "litefs:s82103ee03d64"], "status": "REASONED"}, - "litefs-config": {"text": "Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion.", "components": ["litefs"], "sources": ["litefs:s475c28fd949e", "litefs:s055e15e84ba5", "litefs:s0b0bac964067"], "status": "REASONED"}, - "import": {"text": "POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status.", "components": ["litefs"], "sources": ["litefs:s326a1007e5c2", "litefs:s4547cc32557d", "litefs:s85ece0e83d43"], "status": "REASONED"}, - "export": {"text": "GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404.", "components": ["litefs"], "sources": ["litefs:sa467eeb47f34", "litefs:s5ffa5c20fc78"], "status": "REASONED"}, - "stream": {"text": "POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots.", "components": ["litefs"], "sources": ["litefs:s02908966756d", "litefs:sd63fa776fbc1", "litefs:s1c51b7efc42d", "litefs:scd0168363c1a"], "status": "REASONED"}, - "tx": {"text": "POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404.", "components": ["litefs"], "sources": ["litefs:s298808a10d50", "litefs:s7269cca8b0ea", "litefs:sbd59a1af784e"], "status": "REASONED"}, - "halt": {"text": "POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id.", "components": ["litefs"], "sources": ["litefs:sa199174f541b", "litefs:s750403129022", "litefs:s5009045b97e1"], "status": "REASONED"}, - "halt-ttl": {"text": "LiteFS halt-lock TTL defaults to 30 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"}, - "promote": {"text": "POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:sae4ece3fbc87"], "status": "REASONED"}, - "handoff": {"text": "POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:s7bcb006d6884"], "status": "REASONED"}, - "information": {"text": "GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:se1a41376a98d", "litefs:s93c0ad91a567"], "status": "REASONED"}, - "debug": {"text": "Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s36792457dd64", "litefs:s7234e34af9ef", "go:se49529035a0c", "go:s0246034dddd1"], "status": "REASONED"}, - "debug-rand": {"text": "/debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc"], "status": "REASONED"}, - "cluster-private": {"text": "Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy.", "components": ["litefs", "litefs-docs"], "sources": ["litefs:s694a6ba8eafa", "litefs-docs:sd7c0ba136b25"], "status": "REASONED"}, - "consul-url": {"text": "Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port.", "components": ["litefs"], "sources": ["litefs:s4cebe21e711d"], "status": "REASONED"}, - "static-url": {"text": "Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API.", "components": ["litefs"], "sources": ["litefs:s8dfa3f10f3d3", "litefs:s83d8bd834347", "litefs:sb43f5d78723c"], "status": "REASONED"}, - "container": {"text": "LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability.", "components": ["litefs", "docker"], "sources": ["litefs:s3284d1a3ecad", "docker:s881082f899b2", "docker:s6732393fd414", "docker:s00095bd02553"], "status": "REASONED"}, - "proxy": {"text": "LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary.", "components": ["litefs"], "sources": ["litefs:s2b80fdf8adbe", "litefs:sc0568fe7e0ee", "litefs:sf0d31f900082"], "status": "REASONED"}, - "verify-download": {"text": "Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available.", "components": ["sqlite-rolling", "bash", "curl"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [1]}, - "verify-bundle": {"text": "Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths.", "components": ["turso", "bash", "grep-docs"], "sources": ["turso:sb32fd9c679e1", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "grep-docs:s1733b640052b", "grep-docs:s99f121f5426a", "grep-docs:sbc8d05ab659b", "grep-docs:sfdd7a73eab39", "grep-docs:s7f9dc35e81c5", "grep-docs:s5170317ddea8", "grep-docs:sc08d6af4d642", "grep-docs:scd528c854b46", "grep-docs:sb8be8b716426", "grep-docs:s5d5af9297df2", "grep-docs:se99a83dba3c5"], "status": "DEMONSTRATED", "evidence": "A bundle carrying the placeholder printed `dist/app.js` before `token-literal exit: 0` and again before `jwt-shape exit: 0`, and a clean bundle printed `1` for both.", "verify": [2]}, - "verify-git": {"text": "Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "DEMONSTRATED", "evidence": "With `app.db` committed before an `app.db*` ignore rule was added, `git check-ignore -v` listed the sidecars but not the tracked `app.db`. `git ls-files` printed `app.db`, and `git log` printed its commit.", "verify": [3]}, - "verify-modes": {"text": "SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised.", "components": ["sqlite-rolling", "coreutils"], "sources": ["sqlite-rolling:sf3d7f8391994", "coreutils:s247df4545dc0", "coreutils:sfd49553ab6e5", "coreutils:sbd6f7cad7bf4", "coreutils:s3e55a7242c33"], "status": "REASONED", "verify": [4]}, - "verify-listeners": {"text": "Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available.", "components": ["litefs", "litestream", "mcp", "bash", "curl"], "sources": ["litefs:s694a6ba8eafa", "litestream:sa5dddf04652c", "mcp:sa5dddf04652c", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [5]}, - "verify-turso": {"text": "Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available.", "components": ["turso", "bash", "curl"], "sources": ["turso:se409838a474a", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:sd3b039bafbf6", "curl:s3db522258e44"], "status": "REASONED", "verify": [6]}, - "halt-expiration": {"text": "LiteFS checks halt-lock expiration every 5 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"} - } -} ---- # SQLite in deployment: the file is the exposure (plus Turso and Litestream) - -**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 SQLite documentation (rolling) (no recorded version), Litestream MCP minimum v0.5.0, LiteFS source v0.5.14, Git documentation v2.56.0, GNU Coreutils stat manual v9.7, GNU Bash reference manual bash-5.3, curl curl-8_14_1, and GNU grep documentation v3.12; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 40 claims: 2 demonstrated by a check that ran and 38 reasoned from cited sources. -Components (recorded basis): - -- see: SQLite Encryption Extension (unknown) -- cipher: SQLCipher (unknown) -- turso: Turso documentation (unknown) -- litestream: Litestream documentation (unknown) -- mcp: Litestream MCP minimum (v0.5.0) -- litefs: LiteFS source (v0.5.14) -- litefs-docs: LiteFS documentation (unknown) -- go: Go library documentation (unknown) -- docker: Docker documentation (unknown) -- sqlite-rolling: SQLite documentation (rolling) (unknown) -- git: Git documentation (v2.56.0) -- coreutils: GNU Coreutils stat manual (v9.7) -- bash: GNU Bash reference manual (bash-5.3) -- curl: curl (curl-8_14_1) -- grep-docs: GNU grep documentation (v3.12) - -Claims (full source mapping in the front matter): - -- filesystem (REASONED; sqlite-rolling): No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect. -- extensions (REASONED; sqlite-rolling): Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries. -- web-files (REASONED; sqlite-rolling): Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names. -- git-files (REASONED; sqlite-rolling, git): Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit. -- permissions (REASONED; sqlite-rolling): Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation. -- temporary (REASONED; sqlite-rolling): Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions. -- encryption (REASONED; see, cipher): Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption. -- encryption-keys (REASONED; see, sqlite-rolling): Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak. -- turso-http (REASONED; turso): Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git. -- turso-scope (REASONED; turso): db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens. -- replicas (REASONED; litestream): Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials. -- metrics (REASONED; litestream): Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090. -- mcp (REASONED; mcp): Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001. -- litefs-backups (REASONED; litefs-docs): LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations. -- litefs-transport (REASONED; litefs, go): LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers. -- litefs-bind (REASONED; litefs, go): http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port. -- litefs-config (REASONED; litefs): Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion. -- import (REASONED; litefs): POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status. -- export (REASONED; litefs): GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404. -- stream (REASONED; litefs): POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots. -- tx (REASONED; litefs): POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404. -- halt (REASONED; litefs): POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id. -- halt-ttl (REASONED; litefs): LiteFS halt-lock TTL defaults to 30 seconds. -- promote (REASONED; litefs): POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required. -- handoff (REASONED; litefs): POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required. -- information (REASONED; litefs): GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database. -- debug (REASONED; litefs, go): Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name. -- debug-rand (REASONED; litefs): /debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client. -- cluster-private (REASONED; litefs, litefs-docs): Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy. -- consul-url (REASONED; litefs): Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port. -- static-url (REASONED; litefs): Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API. -- container (REASONED; litefs, docker): LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability. -- proxy (REASONED; litefs): LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary. -- verify-download (REASONED; sqlite-rolling, bash, curl): Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available. -- verify-bundle (DEMONSTRATED; turso, bash, grep-docs): Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths. -- verify-git (DEMONSTRATED; sqlite-rolling): Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded. -- verify-modes (REASONED; sqlite-rolling, coreutils): SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised. -- verify-listeners (REASONED; litefs, litestream, mcp, bash, curl): Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available. -- verify-turso (REASONED; turso, bash, curl): Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available. -- halt-expiration (REASONED; litefs): LiteFS checks halt-lock expiration every 5 seconds. - +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. SQLite has no server process and no network listener: "there are no other processes, threads, machines, or other mechanisms (apart from host computer OS and filesystem) to help provide database services" ([SQLite: serverless](https://www.sqlite.org/serverless.html)), so there is no database-side authentication and the only access control is the filesystem's. Its security page warns that "any database file which might have ever been writable by an agent in a different security domain should be treated as suspect." So the deployment exposure is rarely a flaw in SQLite itself; it is wherever the database file ends up: under a web root, inside a git repository, readable by another local account, or replicated to a public bucket. Run the application with access only to the files it needs, since `ATTACH` opens further databases under the process's filesystem authority; keep extension loading disabled unless required (core SQLite disables it by default, though the SQLite CLI enables it) and load only trusted extension libraries. Application-level SQL-injection prevention is outside this guide's scope. @@ -600,3 +267,362 @@ Applicability checked on 2026-09-18: SQLite 3.x documentation (local file tests - GNU grep documentation v3.12 (checked October 2026): [-f reads one pattern per line; - means stdin (197-207)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197); [-l prints matching file names and stops at the first match per file (342-351)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342); [default binary handling, suppression and matching limits (621-668)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621); [-r recurses, follows command-line symlinks and skips encountered symlinks (757-769)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757); [-- terminates options (788-794)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788); [exit 0 for a match, 1 for none, 2 for error, with the -q exception (1110-1115)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110). - GNU grep documentation v3.12 (checked October 2026): [-E selects extended regular expressions (1142-1148)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142); [-F selects fixed strings (1150-1156)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150); [ordinary regexp characters (1274-1277)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274); [{n,} repeats at least n times (1316-1320)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316); [bracket expressions match one listed character (1363-1373)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363). - curl curl-8_14_1 behaviour used by this guide's probes: the cited `-q`, `-g` and `--noproxy '*'` controls suppress curlrc, URL globbing and proxies; `--connect-timeout 5` bounds connection setup and `--max-time 20` bounds each transfer. The cited `-sS` combination hides progress while retaining errors, `-o /dev/null` suppresses bodies, and `-w` prints the response code, transfer exit code and error text even on failure. HTTP status is separate from transfer success. For Turso, the cited `--data` selects POST and `-H 'Content-Type: application/json'` replaces curl's default content type; the request payload and the required anonymous/authorized outcomes remain this guide's procedure using the Turso reference, not curl's guarantee. The cited default TLS checks support the requirement never to add `-k`. + + + +## 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/sqlite.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): + +- sqlite-rolling: SQLite documentation (rolling) (unknown) +- mcp: Litestream MCP minimum (v0.5.0) +- see: SQLite Encryption Extension (unknown) +- turso: Turso documentation (unknown) +- litestream: Litestream documentation (unknown) +- cipher: SQLCipher (unknown) +- litefs: LiteFS source (v0.5.14) +- litefs-docs: LiteFS documentation (unknown) +- go: Go library documentation (unknown) +- docker: Docker documentation (unknown) +- git: Git documentation (v2.56.0) +- coreutils: GNU Coreutils stat manual (v9.7) +- bash: GNU Bash reference manual (bash-5.3) +- curl: curl (curl-8_14_1) +- grep-docs: GNU grep documentation (v3.12) + +Claims (full source mapping in the JSON block below): + +- filesystem (REASONED; sqlite-rolling): No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect. +- extensions (REASONED; sqlite-rolling): Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries. +- web-files (REASONED; sqlite-rolling): Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names. +- git-files (REASONED; sqlite-rolling, git): Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit. +- permissions (REASONED; sqlite-rolling): Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation. +- temporary (REASONED; sqlite-rolling): Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions. +- encryption (REASONED; see, cipher): Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption. +- encryption-keys (REASONED; see, sqlite-rolling): Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak. +- turso-http (REASONED; turso): Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git. +- turso-scope (REASONED; turso): db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens. +- replicas (REASONED; litestream): Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials. +- metrics (REASONED; litestream): Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090. +- mcp (REASONED; mcp): Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001. +- litefs-backups (REASONED; litefs-docs): LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations. +- litefs-transport (REASONED; litefs, go): LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers. +- litefs-bind (REASONED; litefs, go): http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port. +- litefs-config (REASONED; litefs): Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion. +- import (REASONED; litefs): POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status. +- export (REASONED; litefs): GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404. +- stream (REASONED; litefs): POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots. +- tx (REASONED; litefs): POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404. +- halt (REASONED; litefs): POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id. +- halt-ttl (REASONED; litefs): LiteFS halt-lock TTL defaults to 30 seconds. +- promote (REASONED; litefs): POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required. +- handoff (REASONED; litefs): POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required. +- information (REASONED; litefs): GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database. +- debug (REASONED; litefs, go): Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name. +- debug-rand (REASONED; litefs): /debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client. +- cluster-private (REASONED; litefs, litefs-docs): Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy. +- consul-url (REASONED; litefs): Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port. +- static-url (REASONED; litefs): Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API. +- container (REASONED; litefs, docker): LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability. +- proxy (REASONED; litefs): LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary. +- verify-download (REASONED; sqlite-rolling, bash, curl): Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available. +- verify-bundle (DEMONSTRATED; turso, bash, grep-docs): Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths. +- verify-git (DEMONSTRATED; sqlite-rolling): Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded. +- verify-modes (REASONED; sqlite-rolling, coreutils): SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised. +- verify-listeners (REASONED; litefs, litestream, mcp, bash, curl): Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available. +- verify-turso (REASONED; turso, bash, curl): Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available. +- halt-expiration (REASONED; litefs): LiteFS checks halt-lock expiration every 5 seconds. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "1df2b900bb4797e8f2561a2cb2066c6c74bf8505a9043279c660e1d96229198f", + "components": { + "sqlite-rolling": { + "name": "SQLite documentation (rolling)", + "basis": "unknown", + "sources": { + "s2721da91f8d2": "https://www.sqlite.org/serverless.html", + "sf3d7f8391994": "https://www.sqlite.org/tempfiles.html", + "s742deb3338bb": "https://sqlite.org/loadext.html", + "sadf0431c1271": "https://www.sqlite.org/security.html" + } + }, + "mcp": { + "name": "Litestream MCP minimum", + "basis": "v0.5.0", + "sources": { + "sa5dddf04652c": "https://litestream.io/reference/config/" + } + }, + "see": { + "name": "SQLite Encryption Extension", + "basis": "unknown", + "sources": { + "sda3d8876503d": "https://www.sqlite.org/see/doc/trunk/www/readme.wiki" + } + }, + "turso": { + "name": "Turso documentation", + "basis": "unknown", + "sources": { + "sb32fd9c679e1": "https://docs.turso.tech/sdk/http/quickstart", + "sfec4e734f423": "https://docs.turso.tech/cli/db/tokens/create", + "se409838a474a": "https://docs.turso.tech/sdk/http/reference" + } + }, + "litestream": { + "name": "Litestream documentation", + "basis": "unknown", + "sources": { + "sec3d72af1447": "https://litestream.io/guides/", + "s87fa51accafc": "https://litestream.io/guides/s3/", + "sa5dddf04652c": "https://litestream.io/reference/config/" + } + }, + "cipher": { + "name": "SQLCipher", + "basis": "unknown", + "sources": { + "s57d83a42a74a": "https://www.zetetic.net/sqlcipher/" + } + }, + "litefs": { + "name": "LiteFS source", + "basis": "v0.5.14", + "sources": { + "s694a6ba8eafa": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L134-L268", + "s864cacb8e7cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L76-L99", + "s16da1761d6cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/client.go#L32-L43", + "sf0e982d0e36a": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L57", + "s9389d639da6b": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L54-L58", + "s475c28fd949e": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L76-L110", + "s055e15e84ba5": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L219-L233", + "s0b0bac964067": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L288-L333", + "s326a1007e5c2": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L294-L318", + "s4547cc32557d": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2779-L2811", + "s85ece0e83d43": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1904-L1910", + "sa467eeb47f34": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L320-L346", + "s5ffa5c20fc78": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2682-L2776", + "s02908966756d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L495-L520", + "sd63fa776fbc1": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L526-L585", + "s1c51b7efc42d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L686-L699", + "scd0168363c1a": "https://github.com/superfly/litefs/blob/v0.5.14/http/http.go#L15-L43", + "s298808a10d50": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L461-L493", + "s7269cca8b0ea": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2387-L2448", + "sbd59a1af784e": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2452-L2609", + "sa199174f541b": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L348-L407", + "s750403129022": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L215-L325", + "s5009045b97e1": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1525-L1531", + "se2add54cf935": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L39-L41", + "s59a2dfccd853": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1455-L1465", + "s246e6ad0d330": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L409-L459", + "sae4ece3fbc87": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L408-L432", + "s7bcb006d6884": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L168-L170", + "se1a41376a98d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L271-L292", + "s93c0ad91a567": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L779-L803", + "s36792457dd64": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L487-L488", + "s7234e34af9ef": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1661-L1713", + "s4cebe21e711d": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L329-L348", + "sb43f5d78723c": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L241-L244", + "s8dfa3f10f3d3": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L99-L131", + "s83d8bd834347": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1384-L1387", + "s3284d1a3ecad": "https://github.com/superfly/litefs/blob/v0.5.14/Dockerfile", + "s2b80fdf8adbe": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L519-L524", + "sc0568fe7e0ee": "https://github.com/superfly/litefs/blob/v0.5.14/http/proxy_server.go#L131-L144", + "sf0d31f900082": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L60-L68", + "s82103ee03d64": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L32-L35" + } + }, + "litefs-docs": { + "name": "LiteFS documentation", + "basis": "unknown", + "sources": { + "s13b9e92d9996": "https://fly.io/docs/litefs/", + "sd7c0ba136b25": "https://fly.io/docs/litefs/config/" + } + }, + "go": { + "name": "Go library documentation", + "basis": "unknown", + "sources": { + "s6c5be8454523": "https://pkg.go.dev/github.com/golang/net/http2/h2c#NewHandler", + "s37204ff1b27c": "https://pkg.go.dev/net#Listen", + "se49529035a0c": "https://pkg.go.dev/expvar", + "s0246034dddd1": "https://pkg.go.dev/net/http/pprof" + } + }, + "docker": { + "name": "Docker documentation", + "basis": "unknown", + "sources": { + "s881082f899b2": "https://docs.docker.com/engine/network/drivers/bridge/", + "s6732393fd414": "https://docs.docker.com/engine/network/drivers/host/", + "s00095bd02553": "https://docs.docker.com/engine/network/#container-networks" + } + }, + "git": { + "name": "Git documentation", + "basis": "v2.56.0", + "sources": { + "s12aad6467212": "https://github.com/git/git/blob/v2.56.0/Documentation/gitignore.adoc#L15-L18", + "s8054ab09dc74": "https://github.com/git/git/blob/v2.56.0/Documentation/git-rm.adoc#L17-L28", + "s9a6802adec89": "https://github.com/git/git/blob/v2.56.0/Documentation/git-log.adoc#L16-L16" + } + }, + "coreutils": { + "name": "GNU Coreutils stat manual", + "basis": "v9.7", + "sources": { + "s247df4545dc0": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L12989-L12995", + "sfd49553ab6e5": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13046", + "sbd6f7cad7bf4": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13062", + "s3e55a7242c33": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13073" + } + }, + "bash": { + "name": "GNU Bash reference manual", + "basis": "bash-5.3", + "sources": { + "s3711969be580": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n450", + "s7fec9b923488": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n769", + "s77b19e384d70": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1397", + "se4f37e01a249": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1866", + "s07f77f16f9f5": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2288", + "sbed8cd562a35": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2993", + "s7c9e44003867": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3131", + "s223b5c4d9a28": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4350", + "se02ece9163d4": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4582", + "s6067b2b1f78a": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5216", + "sa506f1abd6a1": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6182", + "s7d29125b4005": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n8232", + "s8962e63cfb20": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2580", + "s8a27e686ddfb": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5466", + "s213f6d8d1f07": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3280", + "sf9757e9fd6e8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3872", + "s8cbc1b4709e2": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4737", + "s3255eca7e496": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4852", + "s252aa2e81179": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5156", + "s8447036b2114": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5161", + "s5cab49305f3d": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5548", + "s6e8ae812c1c3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5625", + "sb38bf74a89b0": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5629", + "s260459961fbc": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5659", + "s8c59508bd4c8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5917", + "s8016d1aecfca": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5929", + "se62fd3ad4fc3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6122", + "s2df39d2c9c1c": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6194", + "s80e3246e96d8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n7274" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5b71321b79ea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/disable.md#L4-L20", + "sc405a4bffc27": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/globoff.md#L4-L22", + "s345ec65025b0": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/noproxy.md#L18-L28", + "sc9973c4ceec1": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/connect-timeout.md#L19-L28", + "s7b6dca376156": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/max-time.md#L21-L27", + "s156481f6b726": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/silent.md#L4-L25", + "s07916104bf60": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/show-error.md#L4-L19", + "sa937dbe5dd4a": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L4-L33", + "s021603e35e68": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L49-L53", + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s39402f9cb25c": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L105-L107", + "sf2dcd83157a6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/insecure.md#L4-L30", + "s13f463154cea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/fail.md#L24-L28", + "s2f1ff2f6cacd": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/output.md#L4-L53", + "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" + } + }, + "grep-docs": { + "name": "GNU grep documentation", + "basis": "v3.12", + "sources": { + "s1733b640052b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197", + "s99f121f5426a": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342", + "sbc8d05ab659b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621", + "sfdd7a73eab39": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757", + "s7f9dc35e81c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788", + "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", + "sc08d6af4d642": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142", + "scd528c854b46": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150", + "sb8be8b716426": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274", + "s5d5af9297df2": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316", + "se99a83dba3c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363" + } + } + }, + "claims": { + "filesystem": {"text": "No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sadf0431c1271"], "status": "REASONED"}, + "extensions": {"text": "Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s742deb3338bb"], "status": "REASONED"}, + "web-files": {"text": "Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "git-files": {"text": "Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit.", "components": ["sqlite-rolling", "git"], "sources": ["sqlite-rolling:sf3d7f8391994", "git:s12aad6467212", "git:s8054ab09dc74", "git:s9a6802adec89"], "status": "REASONED"}, + "permissions": {"text": "Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "temporary": {"text": "Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "encryption": {"text": "Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption.", "components": ["see", "cipher"], "sources": ["see:sda3d8876503d", "cipher:s57d83a42a74a"], "status": "REASONED"}, + "encryption-keys": {"text": "Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak.", "components": ["see", "sqlite-rolling"], "sources": ["see:sda3d8876503d", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "turso-http": {"text": "Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git.", "components": ["turso"], "sources": ["turso:sb32fd9c679e1"], "status": "REASONED"}, + "turso-scope": {"text": "db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens.", "components": ["turso"], "sources": ["turso:sfec4e734f423"], "status": "REASONED"}, + "replicas": {"text": "Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials.", "components": ["litestream"], "sources": ["litestream:sec3d72af1447", "litestream:s87fa51accafc"], "status": "REASONED"}, + "metrics": {"text": "Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090.", "components": ["litestream"], "sources": ["litestream:sa5dddf04652c"], "status": "REASONED"}, + "mcp": {"text": "Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001.", "components": ["mcp"], "sources": ["mcp:sa5dddf04652c"], "status": "REASONED"}, + "litefs-backups": {"text": "LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations.", "components": ["litefs-docs"], "sources": ["litefs-docs:s13b9e92d9996"], "status": "REASONED"}, + "litefs-transport": {"text": "LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc", "litefs:s16da1761d6cc", "go:s6c5be8454523"], "status": "REASONED"}, + "litefs-bind": {"text": "http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port.", "components": ["litefs", "go"], "sources": ["litefs:sf0e982d0e36a", "litefs:s9389d639da6b", "go:s37204ff1b27c", "litefs:s82103ee03d64"], "status": "REASONED"}, + "litefs-config": {"text": "Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion.", "components": ["litefs"], "sources": ["litefs:s475c28fd949e", "litefs:s055e15e84ba5", "litefs:s0b0bac964067"], "status": "REASONED"}, + "import": {"text": "POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status.", "components": ["litefs"], "sources": ["litefs:s326a1007e5c2", "litefs:s4547cc32557d", "litefs:s85ece0e83d43"], "status": "REASONED"}, + "export": {"text": "GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404.", "components": ["litefs"], "sources": ["litefs:sa467eeb47f34", "litefs:s5ffa5c20fc78"], "status": "REASONED"}, + "stream": {"text": "POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots.", "components": ["litefs"], "sources": ["litefs:s02908966756d", "litefs:sd63fa776fbc1", "litefs:s1c51b7efc42d", "litefs:scd0168363c1a"], "status": "REASONED"}, + "tx": {"text": "POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404.", "components": ["litefs"], "sources": ["litefs:s298808a10d50", "litefs:s7269cca8b0ea", "litefs:sbd59a1af784e"], "status": "REASONED"}, + "halt": {"text": "POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id.", "components": ["litefs"], "sources": ["litefs:sa199174f541b", "litefs:s750403129022", "litefs:s5009045b97e1"], "status": "REASONED"}, + "halt-ttl": {"text": "LiteFS halt-lock TTL defaults to 30 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"}, + "promote": {"text": "POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:sae4ece3fbc87"], "status": "REASONED"}, + "handoff": {"text": "POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:s7bcb006d6884"], "status": "REASONED"}, + "information": {"text": "GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:se1a41376a98d", "litefs:s93c0ad91a567"], "status": "REASONED"}, + "debug": {"text": "Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s36792457dd64", "litefs:s7234e34af9ef", "go:se49529035a0c", "go:s0246034dddd1"], "status": "REASONED"}, + "debug-rand": {"text": "/debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc"], "status": "REASONED"}, + "cluster-private": {"text": "Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy.", "components": ["litefs", "litefs-docs"], "sources": ["litefs:s694a6ba8eafa", "litefs-docs:sd7c0ba136b25"], "status": "REASONED"}, + "consul-url": {"text": "Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port.", "components": ["litefs"], "sources": ["litefs:s4cebe21e711d"], "status": "REASONED"}, + "static-url": {"text": "Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API.", "components": ["litefs"], "sources": ["litefs:s8dfa3f10f3d3", "litefs:s83d8bd834347", "litefs:sb43f5d78723c"], "status": "REASONED"}, + "container": {"text": "LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability.", "components": ["litefs", "docker"], "sources": ["litefs:s3284d1a3ecad", "docker:s881082f899b2", "docker:s6732393fd414", "docker:s00095bd02553"], "status": "REASONED"}, + "proxy": {"text": "LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary.", "components": ["litefs"], "sources": ["litefs:s2b80fdf8adbe", "litefs:sc0568fe7e0ee", "litefs:sf0d31f900082"], "status": "REASONED"}, + "verify-download": {"text": "Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available.", "components": ["sqlite-rolling", "bash", "curl"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [1]}, + "verify-bundle": {"text": "Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths.", "components": ["turso", "bash", "grep-docs"], "sources": ["turso:sb32fd9c679e1", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "grep-docs:s1733b640052b", "grep-docs:s99f121f5426a", "grep-docs:sbc8d05ab659b", "grep-docs:sfdd7a73eab39", "grep-docs:s7f9dc35e81c5", "grep-docs:s5170317ddea8", "grep-docs:sc08d6af4d642", "grep-docs:scd528c854b46", "grep-docs:sb8be8b716426", "grep-docs:s5d5af9297df2", "grep-docs:se99a83dba3c5"], "status": "DEMONSTRATED", "evidence": "A bundle carrying the placeholder printed `dist/app.js` before `token-literal exit: 0` and again before `jwt-shape exit: 0`, and a clean bundle printed `1` for both.", "verify": [2]}, + "verify-git": {"text": "Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "DEMONSTRATED", "evidence": "With `app.db` committed before an `app.db*` ignore rule was added, `git check-ignore -v` listed the sidecars but not the tracked `app.db`. `git ls-files` printed `app.db`, and `git log` printed its commit.", "verify": [3]}, + "verify-modes": {"text": "SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised.", "components": ["sqlite-rolling", "coreutils"], "sources": ["sqlite-rolling:sf3d7f8391994", "coreutils:s247df4545dc0", "coreutils:sfd49553ab6e5", "coreutils:sbd6f7cad7bf4", "coreutils:s3e55a7242c33"], "status": "REASONED", "verify": [4]}, + "verify-listeners": {"text": "Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available.", "components": ["litefs", "litestream", "mcp", "bash", "curl"], "sources": ["litefs:s694a6ba8eafa", "litestream:sa5dddf04652c", "mcp:sa5dddf04652c", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [5]}, + "verify-turso": {"text": "Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available.", "components": ["turso", "bash", "curl"], "sources": ["turso:se409838a474a", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:sd3b039bafbf6", "curl:s3db522258e44"], "status": "REASONED", "verify": [6]}, + "halt-expiration": {"text": "LiteFS checks halt-lock expiration every 5 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"} + } +} +```` + diff --git a/plugin/skills/secure-deployment-config/references/transactional-email-posture.md b/plugin/skills/secure-deployment-config/references/transactional-email-posture.md index 8f3d7639f..4b06f8214 100644 --- a/plugin/skills/secure-deployment-config/references/transactional-email-posture.md +++ b/plugin/skills/secure-deployment-config/references/transactional-email-posture.md @@ -1,5 +1,238 @@ ---- -version_basis: { +# Transactional email posture: authenticating the mail your deployment sends + +Written against SPF RFC 7208, DKIM RFC 6376, DKIM cryptography RFC 8301, DMARC RFC 9989, DMARC aggregate reporting RFC 9990, DMARC failure reporting RFC 9991, Submission TLS RFC 8314, and SMTP submission roles RFC 6409, plus 7 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-05; 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. + +Password resets, sign-in links, and invitations are part of your authentication path: a recipient who trusts a forged one hands an attacker the account. The controls that stop that forgery are not a single service's settings but three DNS-published standards (SPF, DKIM, and DMARC), the submission channel your application sends through, and the transport policy between mail servers. This guide covers that posture for a deployment that sends transactional mail. It is standards-based rather than product-based, so [authentication.md](authentication.md) covers the account side and [secrets.md](secrets.md) covers the SMTP credential and DKIM private key you will handle here. + +The through-line: a spoofable sender undermines every message your users are trained to act on, an open relay burns your sending reputation and gets your mail blocked, and an authenticated but plaintext submission leaks the credential that lets an attacker send as you. All values below (domains, IPs, selectors, keys) are illustrative; replace them with your own deployment inventory. + +## 1. SPF: authorize the sending IPs + +SPF publishes, as a DNS TXT record, the IP addresses allowed to send for a domain. A receiver checks the record against the connecting IP and the envelope `MAIL FROM` domain (the `HELO` name for a null return path); it does not check the visible `From:` header, which is why SPF alone does not stop header spoofing and only becomes useful once DMARC ties it to `From:`. + +```dns +bounce.example.com. 3600 IN TXT "v=spf1 ip4:192.0.2.10 include:_spf.provider.example -all" +``` + +The final mechanism decides unmatched senders: `-all` fails them, `~all` softfails (many receivers still deliver a softfail), and `+all` or a bare `all` passes everyone, which destroys the record's meaning. Publish exactly one SPF record per evaluated domain; a domain with two `v=spf1` records is a `permerror`. SPF also caps the terms that trigger DNS lookups (`include`, `a`, `mx`, `ptr`, `exists`, and the `redirect` modifier) at ten across the whole recursion, and exceeding that is a `permerror` that voids the result; `ip4`, `ip6`, and `all` do not count against the ten (RFC 7208 sections 4.5 and 4.6.4). A long chain of provider `include`s is the usual way a domain silently blows the budget: any evaluation that has to walk the whole chain returns `permerror`, though a sender matched by an earlier mechanism such as a listed `ip4` still passes, because evaluation stops at the first match. A `permerror` is not an SPF pass, so legitimate mail that reached it and relied on SPF alignment fails your own DMARC. It breaks your authentication, not an attacker's. + +What remains unverified until you inspect your own zone: the real envelope domain, the provider `include` targets, the live recursive lookup count, and whether a second SPF record was left behind by an earlier vendor. + +## 2. DKIM: sign with a rotatable key + +DKIM attaches a cryptographic signature to each message and publishes the matching public key at a selector under the signing domain. The signature names its domain as `d=` and its selector as `s=`; the receiver fetches the key from the selector record under `_domainkey` (here `tx202609._domainkey.example.com`) to verify it. DKIM proves the signed headers and body were not altered and were authorized by the `d=` domain; it does not encrypt the message. + +```dns +tx202609._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." +``` + +The `p=` value above is a truncated illustrative key; publish the full base64 your signer generates. Sign with `rsa-sha256`: RFC 8301 prohibits `rsa-sha1` outright, sets a hard floor of 1024-bit RSA keys, and recommends 2048-bit or larger, so treat 2048-bit as the working minimum rather than the 1024-bit floor. Rotate by publishing a new selector's public key first, switching signing to it, and only then retiring the old selector once mail signed with it has cleared the network; an empty `p=` revokes a key. Publishing a key is not enough on its own: the sending service must actually sign, and a receiver must actually verify, before DKIM contributes to a DMARC pass. + +Unverified until checked in your deployment: whether signing is enabled, the live `d=` and selector, the key length, whether the key is a TXT record or a provider CNAME delegation, and who owns rotation. + +## 3. DMARC: align authentication to the visible From and enforce + +DMARC is the record that finally protects the `From:` your users see. A message passes DMARC when at least one of SPF or DKIM both passes and is aligned: SPF alignment compares the envelope domain, DKIM alignment compares the verified signature's `d=`, each against the visible `From:` domain. Relaxed alignment (`adkim=r`, `aspf=r`, the default) accepts the same organizational domain; strict (`s`) requires an exact match. + +```dns +_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; sp=reject; np=reject; adkim=r; aspf=r; rua=mailto:dmarc-agg@example.com" +``` + +`p=` sets the requested handling of failures: `none` asks for no change (monitor only, and still spoofable), `quarantine` asks receivers to treat failures as suspicious, and `reject` asks them to reject at SMTP time. `sp=` sets the policy for existing subdomains and `np=` for non-existent ones; where a tag is absent, policy falls back `np` to `sp` to `p`, so a `p=reject` domain already rejects for its own subdomains without them. Publish `sp` and `np` when you want subdomains held to a policy of their own, whether it repeats or differs from `p`, and inventory any subdomain that publishes its own DMARC record, since a receiver's tree walk finds that record first. DMARC discovers the applicable policy and organizational domain by a DNS tree walk (RFC 9989 section 4.10), which replaced the old public-suffix-list method. `t=y` weakens enforcement for testing and `t=n` is the default; the enforcement record above omits it. + +Reach `p=reject` only after a monitoring period under `p=none` with aggregate reports confirms every legitimate stream aligns, or you will reject your own mail. Missing DMARC or `p=none` means no enforcement is requested, not that mail is guaranteed to arrive; receivers keep their own local policy. One legacy note: RFC 9989 (May 2026), which now defines DMARC and obsoletes RFC 7489, removed the `pct` tag, so a `pct` value is not a current rollout control, and a legacy `p=reject; pct=25` was partial application rather than full enforcement. + +Unverified until checked: which streams actually align, the inherited and explicit subdomain policies, and whether your receivers implement the current specification. + +## 4. DMARC reporting: aggregate now, failure reports later + +Aggregate reporting (`rua=`, RFC 9990) sends a daily XML summary of authentication results and is how you see what is sending as your domain before you enforce; turn it on first. A destination in another domain must authorize the reports, or receivers will not send them: + +```dns +example.com._report._dmarc.collector.example.net. IN TXT "v=DMARC1" +``` + +Failure reporting (`ruf=`, RFC 9991) sends per-message forensic copies that can include message content and recipient data, which for password-reset mail can mean leaking a live reset token to whatever inbox collects the reports. Omit `ruf` at first, and enable it only after you have settled collection, redaction, retention, and access, and authorized any external destination the same way. Publication alone is not evidence reports arrive: verify the mailbox actually receives and processes them. + +## 5. SMTP submission and relay: TLS before AUTH, and never an open relay + +Where your application hands mail to a mail server, three ports carry different contracts. Port 587 is submission with STARTTLS: the client upgrades to TLS and then authenticates, but the STARTTLS offer alone does not force encryption, so an opportunistic client that falls back to plaintext is exposed to a STARTTLS-stripping downgrade that captures the credential. Port 465 is submission with implicit TLS: the connection is encrypted from the first byte and is immune to stripping, which is why RFC 8314 prefers it going forward. Require successful TLS with certificate chain and hostname validation before any credential is sent on either, and treat TLS 1.2 as the floor (RFC 8997). Port 25 is inter-server relay or, at some providers, submission; "port 25 means no authentication" is wrong (RFC 6409 section 3.1), so identify which role your endpoint plays rather than assuming. + +An open relay forwards mail from an unauthenticated, untrusted client to a destination the server is not responsible for; it will be found and abused, and your domain blocked. Reject that forwarding while still allowing the two legitimate cases: ordinary unauthenticated inbound delivery to your own local recipients, and explicitly authorized relay paths (RFC 2505 section 2.1). After TLS, an unauthenticated or invalidly authenticated submission must fail and an authorized one succeed. + +Unverified until checked: the live endpoints and ports, whether the client enforces TLS rather than merely offering it, the authentication mechanism, and the relay access rules. + +## 6. Transport and display: MTA-STS, TLS-RPT, and BIMI + +MTA-STS (RFC 8461) lets a receiving domain demand TLS for mail sent to it. A TXT record points to an HTTPS-hosted policy: + +```dns +_mta-sts.example.com. IN TXT "v=STSv1; id=2026091501" +``` + +The policy is served as `text/plain` at `https://mta-sts.example.com/.well-known/mta-sts.txt`: + +```text +version: STSv1 +mode: enforce +mx: mx1.example.com +max_age: 604800 +``` + +Only `mode: enforce` constrains delivery (`testing` and `none` do not), and the crucial caveat is direction: MTA-STS protects mail arriving at the policy domain, so publishing it does not secure your own outbound mail to other domains, and it depends on your sending server validating recipients' policies. Update the `id` whenever the policy changes. TLS-RPT (RFC 8460) is reporting only, never enforcement: + +```dns +_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-rpt@example.com" +``` + +BIMI is optional display metadata that shows a brand logo in supporting clients; it depends on DMARC enforcement already being in place and is not an authentication mechanism or a guarantee the logo renders. Keep it last, after the records above are enforcing. + +## Verify + +REASONED: following block; DNS publication, submission TLS and plaintext AUTH refusal, per the guide's cited BIND 9.18.39 dig manual, OpenSSL 3.0 s_client, RFC 8314: Cleartext Considered Obsolete and RFC 8997 section 3. No controlled DNS zone or SMTP endpoint is available, and local listeners are denied. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; openssl s_client -help exited 0 and listed -starttls, -verify_hostname, -verify_return_error and -min_protocol. Published records should match the configured policies; exposed submission accepts plaintext AUTH, while fixed submission refuses it and supports validated TLS before authorized AUTH. These live outcomes remain unobserved. Every probe below is reasoned, not demonstrated: the authoring environment has no DNS control, no SMTP endpoint, and no container runtime, so the outcomes are derived from the cited RFCs rather than observed. For the submission probes, a transport failure or a TLS error is inconclusive, never the fixed state. + +```bash +# REASONED: DNS and TLS expectations follow the cited RFCs; no DNS control, SMTP endpoint, or container runtime is available. +# Inspect the published records (replace names; take the DKIM selector from a real signature). +# These read DNS and TLS state; they do not evaluate SPF or DMARC or prove a service listener. +dig +noall +answer TXT bounce.example.com +dig +noall +answer TXT tx202609._domainkey.example.com +dig +noall +answer CNAME tx202609._domainkey.example.com +dig +noall +answer TXT _dmarc.example.com +dig +noall +answer TXT _mta-sts.example.com +dig +noall +answer TXT _smtp._tls.example.com +dig +noall +answer TXT default._bimi.example.com + +# Submission must complete TLS with a validated certificate before AUTH. -verify_return_error +# matters because s_client otherwise continues past a verification failure. This confirms TLS is +# available, not that AUTH is gated on it; the discriminating check, reasoned here, is the negative +# one: a plaintext or post-stripped-STARTTLS AUTH attempt must be refused, where the exposed state +# accepts it. +openssl s_client -connect smtp.example.com:587 -starttls smtp \ + -servername smtp.example.com -verify_hostname smtp.example.com \ + -verify_return_error -min_protocol TLSv1.2 +## 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/transactional-email-posture.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): + +- spf: SPF (RFC 7208) +- dkim: DKIM (RFC 6376) +- crypto: DKIM cryptography (RFC 8301) +- dmarc: DMARC (RFC 9989) +- aggregate: DMARC aggregate reporting (RFC 9990) +- failure: DMARC failure reporting (RFC 9991) +- submission: Submission TLS (RFC 8314) +- roles: SMTP submission roles (RFC 6409) +- relay: SMTP relay controls (RFC 2505) +- sts: MTA-STS (RFC 8461) +- rpt: TLS-RPT (RFC 8460) +- results: Authentication-Results (RFC 8601) +- bimi: BIMI documentation (unknown) +- submission-update: Submission TLS update (RFC 8997) +- bind: BIND dig manual (9.18.39) +- openssl: OpenSSL command documentation (3.0) + +Claims (full source mapping in the JSON block below): + +- spf-identity (REASONED; spf, dmarc): SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment. +- spf-all (REASONED; spf): -all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone. +- spf-record (REASONED; spf): Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror. +- spf-lookups (REASONED; spf): Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass. +- dkim-signing (REASONED; dkim, dmarc): DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC. +- dkim-crypto (REASONED; crypto): Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example. +- dkim-rotation (REASONED; dkim): Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership. +- dmarc-alignment (REASONED; dmarc): DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching. +- dmarc-policy (REASONED; dmarc): p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery. +- dmarc-subdomains (REASONED; dmarc): sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies. +- dmarc-discovery (REASONED; dmarc): RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method. +- dmarc-testing (REASONED; dmarc): t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag. +- dmarc-rollout (REASONED; dmarc, aggregate): Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected. +- dmarc-pct (REASONED; dmarc): RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control. +- aggregate (REASONED; aggregate): Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked. +- failure-reports (REASONED; failure): ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations. +- starttls (REASONED; submission): 587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS. +- implicit-tls (REASONED; submission): 465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward. +- tls-validation (REASONED; submission, submission-update): Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997. +- port-25 (REASONED; roles): 25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication. +- relay-policy (REASONED; relay): Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths. +- submission-auth (REASONED; roles, submission): After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds. +- sts-publication (REASONED; sts): MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800. +- sts-enforcement (REASONED; sts): Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail. +- sts-refresh (REASONED; sts): Update the MTA-STS TXT id whenever the policy changes. +- tls-reports (REASONED; rpt): TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement. +- bimi (REASONED; bimi): BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last. +- verify-dns (REASONED; spf, dkim, dmarc, sts, rpt, bimi, bind): Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available. +- verify-tls (REASONED; submission, submission-update, openssl): 587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available. +- verify-alignment (REASONED; results, dmarc): Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling. +- verify-relay (REASONED; relay, roles): In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved. + +````json +{ "schema": 1, "checked": "2026-10-05", "documentation_checked": "2026-09", @@ -153,212 +386,5 @@ version_basis: { "verify-relay": {"text": "In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved.", "components": ["relay", "roles"], "sources": ["relay:s1b9349dd99bc", "roles:s3f9972faf8ff"], "status": "REASONED"} } } ---- -# Transactional email posture: authenticating the mail your deployment sends - - -**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): - -- spf: SPF (RFC 7208) -- dkim: DKIM (RFC 6376) -- crypto: DKIM cryptography (RFC 8301) -- dmarc: DMARC (RFC 9989) -- aggregate: DMARC aggregate reporting (RFC 9990) -- failure: DMARC failure reporting (RFC 9991) -- submission: Submission TLS (RFC 8314) -- roles: SMTP submission roles (RFC 6409) -- relay: SMTP relay controls (RFC 2505) -- sts: MTA-STS (RFC 8461) -- rpt: TLS-RPT (RFC 8460) -- results: Authentication-Results (RFC 8601) -- bimi: BIMI documentation (unknown) -- submission-update: Submission TLS update (RFC 8997) -- bind: BIND dig manual (9.18.39) -- openssl: OpenSSL command documentation (3.0) - -Claims (full source mapping in the front matter): - -- spf-identity (REASONED; spf, dmarc): SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment. -- spf-all (REASONED; spf): -all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone. -- spf-record (REASONED; spf): Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror. -- spf-lookups (REASONED; spf): Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass. -- dkim-signing (REASONED; dkim, dmarc): DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC. -- dkim-crypto (REASONED; crypto): Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example. -- dkim-rotation (REASONED; dkim): Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership. -- dmarc-alignment (REASONED; dmarc): DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching. -- dmarc-policy (REASONED; dmarc): p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery. -- dmarc-subdomains (REASONED; dmarc): sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies. -- dmarc-discovery (REASONED; dmarc): RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method. -- dmarc-testing (REASONED; dmarc): t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag. -- dmarc-rollout (REASONED; dmarc, aggregate): Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected. -- dmarc-pct (REASONED; dmarc): RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control. -- aggregate (REASONED; aggregate): Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked. -- failure-reports (REASONED; failure): ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations. -- starttls (REASONED; submission): 587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS. -- implicit-tls (REASONED; submission): 465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward. -- tls-validation (REASONED; submission, submission-update): Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997. -- port-25 (REASONED; roles): 25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication. -- relay-policy (REASONED; relay): Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths. -- submission-auth (REASONED; roles, submission): After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds. -- sts-publication (REASONED; sts): MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800. -- sts-enforcement (REASONED; sts): Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail. -- sts-refresh (REASONED; sts): Update the MTA-STS TXT id whenever the policy changes. -- tls-reports (REASONED; rpt): TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement. -- bimi (REASONED; bimi): BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last. -- verify-dns (REASONED; spf, dkim, dmarc, sts, rpt, bimi, bind): Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available. -- verify-tls (REASONED; submission, submission-update, openssl): 587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available. -- verify-alignment (REASONED; results, dmarc): Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling. -- verify-relay (REASONED; relay, roles): In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved. +```` - -Password resets, sign-in links, and invitations are part of your authentication path: a recipient who trusts a forged one hands an attacker the account. The controls that stop that forgery are not a single service's settings but three DNS-published standards (SPF, DKIM, and DMARC), the submission channel your application sends through, and the transport policy between mail servers. This guide covers that posture for a deployment that sends transactional mail. It is standards-based rather than product-based, so [authentication.md](authentication.md) covers the account side and [secrets.md](secrets.md) covers the SMTP credential and DKIM private key you will handle here. - -The through-line: a spoofable sender undermines every message your users are trained to act on, an open relay burns your sending reputation and gets your mail blocked, and an authenticated but plaintext submission leaks the credential that lets an attacker send as you. All values below (domains, IPs, selectors, keys) are illustrative; replace them with your own deployment inventory. - -## 1. SPF: authorize the sending IPs - -SPF publishes, as a DNS TXT record, the IP addresses allowed to send for a domain. A receiver checks the record against the connecting IP and the envelope `MAIL FROM` domain (the `HELO` name for a null return path); it does not check the visible `From:` header, which is why SPF alone does not stop header spoofing and only becomes useful once DMARC ties it to `From:`. - -```dns -bounce.example.com. 3600 IN TXT "v=spf1 ip4:192.0.2.10 include:_spf.provider.example -all" -``` - -The final mechanism decides unmatched senders: `-all` fails them, `~all` softfails (many receivers still deliver a softfail), and `+all` or a bare `all` passes everyone, which destroys the record's meaning. Publish exactly one SPF record per evaluated domain; a domain with two `v=spf1` records is a `permerror`. SPF also caps the terms that trigger DNS lookups (`include`, `a`, `mx`, `ptr`, `exists`, and the `redirect` modifier) at ten across the whole recursion, and exceeding that is a `permerror` that voids the result; `ip4`, `ip6`, and `all` do not count against the ten (RFC 7208 sections 4.5 and 4.6.4). A long chain of provider `include`s is the usual way a domain silently blows the budget: any evaluation that has to walk the whole chain returns `permerror`, though a sender matched by an earlier mechanism such as a listed `ip4` still passes, because evaluation stops at the first match. A `permerror` is not an SPF pass, so legitimate mail that reached it and relied on SPF alignment fails your own DMARC. It breaks your authentication, not an attacker's. - -What remains unverified until you inspect your own zone: the real envelope domain, the provider `include` targets, the live recursive lookup count, and whether a second SPF record was left behind by an earlier vendor. - -## 2. DKIM: sign with a rotatable key - -DKIM attaches a cryptographic signature to each message and publishes the matching public key at a selector under the signing domain. The signature names its domain as `d=` and its selector as `s=`; the receiver fetches the key from the selector record under `_domainkey` (here `tx202609._domainkey.example.com`) to verify it. DKIM proves the signed headers and body were not altered and were authorized by the `d=` domain; it does not encrypt the message. - -```dns -tx202609._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." -``` - -The `p=` value above is a truncated illustrative key; publish the full base64 your signer generates. Sign with `rsa-sha256`: RFC 8301 prohibits `rsa-sha1` outright, sets a hard floor of 1024-bit RSA keys, and recommends 2048-bit or larger, so treat 2048-bit as the working minimum rather than the 1024-bit floor. Rotate by publishing a new selector's public key first, switching signing to it, and only then retiring the old selector once mail signed with it has cleared the network; an empty `p=` revokes a key. Publishing a key is not enough on its own: the sending service must actually sign, and a receiver must actually verify, before DKIM contributes to a DMARC pass. - -Unverified until checked in your deployment: whether signing is enabled, the live `d=` and selector, the key length, whether the key is a TXT record or a provider CNAME delegation, and who owns rotation. - -## 3. DMARC: align authentication to the visible From and enforce - -DMARC is the record that finally protects the `From:` your users see. A message passes DMARC when at least one of SPF or DKIM both passes and is aligned: SPF alignment compares the envelope domain, DKIM alignment compares the verified signature's `d=`, each against the visible `From:` domain. Relaxed alignment (`adkim=r`, `aspf=r`, the default) accepts the same organizational domain; strict (`s`) requires an exact match. - -```dns -_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; sp=reject; np=reject; adkim=r; aspf=r; rua=mailto:dmarc-agg@example.com" -``` - -`p=` sets the requested handling of failures: `none` asks for no change (monitor only, and still spoofable), `quarantine` asks receivers to treat failures as suspicious, and `reject` asks them to reject at SMTP time. `sp=` sets the policy for existing subdomains and `np=` for non-existent ones; where a tag is absent, policy falls back `np` to `sp` to `p`, so a `p=reject` domain already rejects for its own subdomains without them. Publish `sp` and `np` when you want subdomains held to a policy of their own, whether it repeats or differs from `p`, and inventory any subdomain that publishes its own DMARC record, since a receiver's tree walk finds that record first. DMARC discovers the applicable policy and organizational domain by a DNS tree walk (RFC 9989 section 4.10), which replaced the old public-suffix-list method. `t=y` weakens enforcement for testing and `t=n` is the default; the enforcement record above omits it. - -Reach `p=reject` only after a monitoring period under `p=none` with aggregate reports confirms every legitimate stream aligns, or you will reject your own mail. Missing DMARC or `p=none` means no enforcement is requested, not that mail is guaranteed to arrive; receivers keep their own local policy. One legacy note: RFC 9989 (May 2026), which now defines DMARC and obsoletes RFC 7489, removed the `pct` tag, so a `pct` value is not a current rollout control, and a legacy `p=reject; pct=25` was partial application rather than full enforcement. - -Unverified until checked: which streams actually align, the inherited and explicit subdomain policies, and whether your receivers implement the current specification. - -## 4. DMARC reporting: aggregate now, failure reports later - -Aggregate reporting (`rua=`, RFC 9990) sends a daily XML summary of authentication results and is how you see what is sending as your domain before you enforce; turn it on first. A destination in another domain must authorize the reports, or receivers will not send them: - -```dns -example.com._report._dmarc.collector.example.net. IN TXT "v=DMARC1" -``` - -Failure reporting (`ruf=`, RFC 9991) sends per-message forensic copies that can include message content and recipient data, which for password-reset mail can mean leaking a live reset token to whatever inbox collects the reports. Omit `ruf` at first, and enable it only after you have settled collection, redaction, retention, and access, and authorized any external destination the same way. Publication alone is not evidence reports arrive: verify the mailbox actually receives and processes them. - -## 5. SMTP submission and relay: TLS before AUTH, and never an open relay - -Where your application hands mail to a mail server, three ports carry different contracts. Port 587 is submission with STARTTLS: the client upgrades to TLS and then authenticates, but the STARTTLS offer alone does not force encryption, so an opportunistic client that falls back to plaintext is exposed to a STARTTLS-stripping downgrade that captures the credential. Port 465 is submission with implicit TLS: the connection is encrypted from the first byte and is immune to stripping, which is why RFC 8314 prefers it going forward. Require successful TLS with certificate chain and hostname validation before any credential is sent on either, and treat TLS 1.2 as the floor (RFC 8997). Port 25 is inter-server relay or, at some providers, submission; "port 25 means no authentication" is wrong (RFC 6409 section 3.1), so identify which role your endpoint plays rather than assuming. - -An open relay forwards mail from an unauthenticated, untrusted client to a destination the server is not responsible for; it will be found and abused, and your domain blocked. Reject that forwarding while still allowing the two legitimate cases: ordinary unauthenticated inbound delivery to your own local recipients, and explicitly authorized relay paths (RFC 2505 section 2.1). After TLS, an unauthenticated or invalidly authenticated submission must fail and an authorized one succeed. - -Unverified until checked: the live endpoints and ports, whether the client enforces TLS rather than merely offering it, the authentication mechanism, and the relay access rules. - -## 6. Transport and display: MTA-STS, TLS-RPT, and BIMI - -MTA-STS (RFC 8461) lets a receiving domain demand TLS for mail sent to it. A TXT record points to an HTTPS-hosted policy: - -```dns -_mta-sts.example.com. IN TXT "v=STSv1; id=2026091501" -``` - -The policy is served as `text/plain` at `https://mta-sts.example.com/.well-known/mta-sts.txt`: - -```text -version: STSv1 -mode: enforce -mx: mx1.example.com -max_age: 604800 -``` - -Only `mode: enforce` constrains delivery (`testing` and `none` do not), and the crucial caveat is direction: MTA-STS protects mail arriving at the policy domain, so publishing it does not secure your own outbound mail to other domains, and it depends on your sending server validating recipients' policies. Update the `id` whenever the policy changes. TLS-RPT (RFC 8460) is reporting only, never enforcement: - -```dns -_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-rpt@example.com" -``` - -BIMI is optional display metadata that shows a brand logo in supporting clients; it depends on DMARC enforcement already being in place and is not an authentication mechanism or a guarantee the logo renders. Keep it last, after the records above are enforcing. - -## Verify - -REASONED: following block; DNS publication, submission TLS and plaintext AUTH refusal, per the guide's cited BIND 9.18.39 dig manual, OpenSSL 3.0 s_client, RFC 8314: Cleartext Considered Obsolete and RFC 8997 section 3. No controlled DNS zone or SMTP endpoint is available, and local listeners are denied. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; openssl s_client -help exited 0 and listed -starttls, -verify_hostname, -verify_return_error and -min_protocol. Published records should match the configured policies; exposed submission accepts plaintext AUTH, while fixed submission refuses it and supports validated TLS before authorized AUTH. These live outcomes remain unobserved. Every probe below is reasoned, not demonstrated: the authoring environment has no DNS control, no SMTP endpoint, and no container runtime, so the outcomes are derived from the cited RFCs rather than observed. For the submission probes, a transport failure or a TLS error is inconclusive, never the fixed state. - -```bash -# REASONED: DNS and TLS expectations follow the cited RFCs; no DNS control, SMTP endpoint, or container runtime is available. -# Inspect the published records (replace names; take the DKIM selector from a real signature). -# These read DNS and TLS state; they do not evaluate SPF or DMARC or prove a service listener. -dig +noall +answer TXT bounce.example.com -dig +noall +answer TXT tx202609._domainkey.example.com -dig +noall +answer CNAME tx202609._domainkey.example.com -dig +noall +answer TXT _dmarc.example.com -dig +noall +answer TXT _mta-sts.example.com -dig +noall +answer TXT _smtp._tls.example.com -dig +noall +answer TXT default._bimi.example.com - -# Submission must complete TLS with a validated certificate before AUTH. -verify_return_error -# matters because s_client otherwise continues past a verification failure. This confirms TLS is -# available, not that AUTH is gated on it; the discriminating check, reasoned here, is the negative -# one: a plaintext or post-stripped-STARTTLS AUTH attempt must be refused, where the exposed state -# accepts it. -openssl s_client -connect smtp.example.com:587 -starttls smtp \ - -servername smtp.example.com -verify_hostname smtp.example.com \ - -verify_return_error -min_protocol TLSv1.2 -**Version basis** +Written against nginx documentation (no recorded version), Apache documentation 2.4, Caddy documentation v2.11.4, Next.js browser source maps v16.3.6, Vite production source maps v8.3.1, Debian apache2 packaging 2.4.69-1, ACME HTTP-01 RFC 8555, and GNU grep 3.12; 1 further source is pinned to a commit and 5 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 26 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): - -- nginx: nginx documentation (unknown) -- apache: Apache documentation (2.4) -- caddy: Caddy documentation (v2.11.4) -- next: Next.js documentation (unknown) -- vite: Vite documentation (unknown) -- cra: Create React App documentation (unknown) -- coreutils: GNU coreutils documentation (unknown) -- grep: GNU grep (3.12) -- caddy-rolling: Caddy documentation (rolling) (unknown) -- acme-http01: ACME HTTP-01 (RFC 8555) -- debian-packaging: Debian apache2 packaging (2.4.69-1) -- ecma426: ECMA-426 (c724f0f494b44b21b651fdbd6412d3437eef0e72) -- next-maps: Next.js browser source maps (v16.3.6) -- vite-maps: Vite production source maps (v8.3.1) - -Claims (full source mapping in the front matter): - -- nginx-dotfiles (REASONED; nginx): Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them. -- nginx-backups (REASONED; nginx): Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence. -- nginx-acme (REASONED; nginx): Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets. -- nginx-security-txt (REASONED; nginx): The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations. -- apache-directories (REASONED; apache): DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/. -- apache-files (REASONED; apache): Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config. -- apache-listing (REASONED; apache, debian-packaging): Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide. -- apache-forbidden (REASONED; apache): mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed. -- apache-options (REASONED; apache): Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options. -- nginx-listing (REASONED; nginx): nginx autoindex defaults off. -- caddy-listing (REASONED; caddy-rolling, caddy): Guide says Caddy lists only with file_server browse. -- caddy-deny (REASONED; caddy, caddy-rolling): Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run. -- caddy-acme (REASONED; caddy, caddy-rolling): ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets. -- backup-storage (REASONED; nginx, apache, caddy, caddy-rolling): Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names. -- suffix-policy (REASONED; nginx, apache, caddy, caddy-rolling): Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible. -- next-public (REASONED; next): NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes. -- vite-public (REASONED; vite): VITE_ embeds values in browser JavaScript; reserve for public values. -- cra-public (REASONED; cra): Deprecated Create React App embeds REACT_APP_ values in browser builds. -- source-maps (REASONED; next, vite, ecma426, next-maps, vite-maps): ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public. -- verify-dotfiles (REASONED; nginx, apache, caddy, caddy-rolling): Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes. -- verify-listing (REASONED; apache): Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward. -- verify-acme (REASONED; nginx, apache, acme-http01): Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive. -- verify-lookalike (REASONED; nginx, apache, caddy, caddy-rolling): Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it. -- verify-bundle (REASONED; next, vite, cra, grep): Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets. -- verify-backup (REASONED; nginx, apache, caddy, caddy-rolling): Known previously deployed backup URLs should return 403 or 404 without the file contents. -- verify-suffixes (REASONED; nginx, apache, caddy, coreutils, caddy-rolling): GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution. - +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. Scanners request `/.env`, `/.git/config`, `/config.php.bak`, and `/db.sql` continuously, and any of these under a web root hands over credentials or source regardless of what the application itself authenticates. @@ -455,3 +249,235 @@ done - ECMA-426 c724f0f494b44b21b651fdbd6412d3437eef0e72 data URI linking and sourceMappingURL comments (checked October 2026): https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1294 ; https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1323 ; https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1415 - Next.js v16.3.6 production browser source maps default and opt-in serving (checked October 2026): https://github.com/vercel/next.js/blob/v16.3.6/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18 - Vite v8.3.1 build.sourcemap default and output modes (checked October 2026): https://github.com/vitejs/vite/blob/v8.3.1/docs/config/build-options.md#L145-L150 + + + +## 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/web-exposure.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: nginx documentation (unknown) +- apache: Apache documentation (2.4) +- caddy: Caddy documentation (v2.11.4) +- caddy-rolling: Caddy documentation (rolling) (unknown) +- next: Next.js documentation (unknown) +- vite: Vite documentation (unknown) +- cra: Create React App documentation (unknown) +- next-maps: Next.js browser source maps (v16.3.6) +- vite-maps: Vite production source maps (v8.3.1) +- debian-packaging: Debian apache2 packaging (2.4.69-1) +- acme-http01: ACME HTTP-01 (RFC 8555) +- ecma426: ECMA-426 (c724f0f494b44b21b651fdbd6412d3437eef0e72) +- coreutils: GNU coreutils documentation (unknown) +- grep: GNU grep (3.12) + +Claims (full source mapping in the JSON block below): + +- nginx-dotfiles (REASONED; nginx): Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them. +- nginx-backups (REASONED; nginx): Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence. +- nginx-acme (REASONED; nginx): Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets. +- nginx-security-txt (REASONED; nginx): The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations. +- apache-directories (REASONED; apache): DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/. +- apache-files (REASONED; apache): Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config. +- apache-listing (REASONED; apache, debian-packaging): Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide. +- apache-forbidden (REASONED; apache): mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed. +- apache-options (REASONED; apache): Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options. +- nginx-listing (REASONED; nginx): nginx autoindex defaults off. +- caddy-listing (REASONED; caddy-rolling, caddy): Guide says Caddy lists only with file_server browse. +- caddy-deny (REASONED; caddy, caddy-rolling): Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run. +- caddy-acme (REASONED; caddy, caddy-rolling): ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets. +- backup-storage (REASONED; nginx, apache, caddy, caddy-rolling): Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names. +- suffix-policy (REASONED; nginx, apache, caddy, caddy-rolling): Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible. +- next-public (REASONED; next): NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes. +- vite-public (REASONED; vite): VITE_ embeds values in browser JavaScript; reserve for public values. +- cra-public (REASONED; cra): Deprecated Create React App embeds REACT_APP_ values in browser builds. +- source-maps (REASONED; next, vite, ecma426, next-maps, vite-maps): ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public. +- verify-dotfiles (REASONED; nginx, apache, caddy, caddy-rolling): Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes. +- verify-listing (REASONED; apache): Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward. +- verify-acme (REASONED; nginx, apache, acme-http01): Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive. +- verify-lookalike (REASONED; nginx, apache, caddy, caddy-rolling): Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it. +- verify-bundle (REASONED; next, vite, cra, grep): Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets. +- verify-backup (REASONED; nginx, apache, caddy, caddy-rolling): Known previously deployed backup URLs should return 403 or 404 without the file contents. +- verify-suffixes (REASONED; nginx, apache, caddy, coreutils, caddy-rolling): GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "d04f9d877201565cfdd6fe952b50f26d3d85bd16e74efce72a6ef79cebdee43b", + "components": { + "nginx": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s0e5162d1ce2b": "https://nginx.org/en/docs/http/ngx_http_access_module.html", + "sbd17bad53c9d": "https://nginx.org/en/docs/http/ngx_http_autoindex_module.html", + "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html" + } + }, + "apache": { + "name": "Apache documentation", + "basis": "2.4", + "sources": { + "s24f06763d14a": "https://httpd.apache.org/docs/2.4/mod/core.html#directorymatch", + "se24e9bbc0443": "https://httpd.apache.org/docs/2.4/mod/core.html#filesmatch", + "s97468bf1f299": "https://httpd.apache.org/docs/2.4/mod/core.html#options", + "s7313b1b624d5": "https://httpd.apache.org/docs/2.4/mod/mod_authz_core.html", + "sef5ff46eddf7": "https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html" + } + }, + "caddy": { + "name": "Caddy documentation", + "basis": "v2.11.4", + "sources": { + "s450aa9ed710b": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/matchers.go", + "se8dd8140d6b8": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/fileserver/staticfiles.go#L140-L142", + "s86912a2959e0": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/fileserver/staticfiles.go#L371-L374", + "s128797d9a40f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/server.go#L384-L388" + } + }, + "caddy-rolling": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "se9ab71afc4ba": "https://caddyserver.com/docs/caddyfile/directives/respond", + "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" + } + }, + "next": { + "name": "Next.js documentation", + "basis": "unknown", + "sources": { + "sb98bee5b61c8": "https://nextjs.org/docs/pages/guides/environment-variables" + } + }, + "vite": { + "name": "Vite documentation", + "basis": "unknown", + "sources": { + "s3fc0892e0f58": "https://vite.dev/guide/env-and-mode" + } + }, + "cra": { + "name": "Create React App documentation", + "basis": "unknown", + "sources": { + "scfb24624addd": "https://create-react-app.dev/docs/adding-custom-environment-variables/" + } + }, + "next-maps": { + "name": "Next.js browser source maps", + "basis": "v16.3.6", + "sources": { + "s6964da09fc21": "https://github.com/vercel/next.js/blob/v16.3.6/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" + } + }, + "vite-maps": { + "name": "Vite production source maps", + "basis": "v8.3.1", + "sources": { + "sf12c9114bd79": "https://github.com/vitejs/vite/blob/v8.3.1/docs/config/build-options.md#L145-L150" + } + }, + "debian-packaging": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1", + "s90898e0f03b6": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/apache2.conf.in#L170-L174", + "s6f77b580cc87": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/mods-available/dir.conf#L1", + "s7ea1fcaca182": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.postinst#L38-L52", + "sd7d93d2a9705": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.postinst#L257-L266" + } + }, + "acme-http01": { + "name": "ACME HTTP-01", + "basis": "RFC 8555", + "sources": { + "se60bdd68e41d": "https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3" + } + }, + "ecma426": { + "name": "ECMA-426", + "basis": "c724f0f494b44b21b651fdbd6412d3437eef0e72", + "sources": { + "s5de29cfb3e99": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L584-L585", + "s2af3a1c52b4a": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1294", + "sfeb03b2200f4": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1323", + "s94b6d647e4bc": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1415" + } + }, + "coreutils": { + "name": "GNU coreutils documentation", + "basis": "unknown", + "sources": { + "sabbd081d1d5a": "https://www.gnu.org/s/coreutils/manual/html_node/mktemp-invocation.html" + } + }, + "grep": { + "name": "GNU grep", + "basis": "3.12", + "sources": { + "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", + "s057470125f4c": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n425", + "sb18b36e247d7": "https://git.savannah.gnu.org/cgit/grep.git/tree/src/grep.c?h=v3.12#n1756", + "s0c4ddbee9e68": "https://git.savannah.gnu.org/cgit/grep.git/tree/src/grep.c?h=v3.12#n661" + } + } + }, + "claims": { + "nginx-dotfiles": {"text": "Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-backups": {"text": "Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-acme": {"text": "Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-security-txt": {"text": "The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "apache-directories": {"text": "DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/.", "components": ["apache"], "sources": ["apache:s24f06763d14a", "apache:s7313b1b624d5", "apache:se24e9bbc0443"], "status": "REASONED"}, + "apache-files": {"text": "Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config.", "components": ["apache"], "sources": ["apache:se24e9bbc0443", "apache:s7313b1b624d5"], "status": "REASONED"}, + "apache-listing": {"text": "Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide.", "components": ["apache", "debian-packaging"], "sources": ["apache:s97468bf1f299", "apache:sef5ff46eddf7", "debian-packaging:s62f6ed2866ab", "debian-packaging:s90898e0f03b6", "debian-packaging:s6f77b580cc87", "debian-packaging:s7ea1fcaca182", "debian-packaging:sd7d93d2a9705"], "status": "REASONED"}, + "apache-forbidden": {"text": "mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed.", "components": ["apache"], "sources": ["apache:sef5ff46eddf7"], "status": "REASONED"}, + "apache-options": {"text": "Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options.", "components": ["apache"], "sources": ["apache:s97468bf1f299"], "status": "REASONED"}, + "nginx-listing": {"text": "nginx autoindex defaults off.", "components": ["nginx"], "sources": ["nginx:sbd17bad53c9d"], "status": "REASONED"}, + "caddy-listing": {"text": "Guide says Caddy lists only with file_server browse.", "components": ["caddy-rolling", "caddy"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:se8dd8140d6b8", "caddy:s86912a2959e0"], "status": "REASONED"}, + "caddy-deny": {"text": "Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run.", "components": ["caddy", "caddy-rolling"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "caddy-acme": {"text": "ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets.", "components": ["caddy", "caddy-rolling"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b", "caddy:s128797d9a40f"], "status": "REASONED"}, + "backup-storage": {"text": "Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "suffix-policy": {"text": "Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "next-public": {"text": "NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes.", "components": ["next"], "sources": ["next:sb98bee5b61c8"], "status": "REASONED"}, + "vite-public": {"text": "VITE_ embeds values in browser JavaScript; reserve for public values.", "components": ["vite"], "sources": ["vite:s3fc0892e0f58"], "status": "REASONED"}, + "cra-public": {"text": "Deprecated Create React App embeds REACT_APP_ values in browser builds.", "components": ["cra"], "sources": ["cra:scfb24624addd"], "status": "REASONED"}, + "source-maps": {"text": "ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public.", "components": ["next", "vite", "ecma426", "next-maps", "vite-maps"], "sources": ["next:sb98bee5b61c8", "vite:s3fc0892e0f58", "ecma426:s5de29cfb3e99", "ecma426:s2af3a1c52b4a", "ecma426:sfeb03b2200f4", "ecma426:s94b6d647e4bc", "next-maps:s6964da09fc21", "vite-maps:sf12c9114bd79"], "status": "REASONED"}, + "verify-dotfiles": {"text": "Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:s24f06763d14a", "apache:s7313b1b624d5", "apache:se24e9bbc0443", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED", "verify": [1]}, + "verify-listing": {"text": "Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward.", "components": ["apache"], "sources": ["apache:s97468bf1f299", "apache:sef5ff46eddf7"], "status": "REASONED", "verify": [1]}, + "verify-acme": {"text": "Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive.", "components": ["nginx", "apache", "acme-http01"], "sources": ["nginx:s40bdf1af1596", "nginx:s0e5162d1ce2b", "apache:s24f06763d14a", "acme-http01:se60bdd68e41d"], "status": "REASONED", "verify": [1]}, + "verify-lookalike": {"text": "Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:s24f06763d14a", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED", "verify": [1]}, + "verify-bundle": {"text": "Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets.", "components": ["next", "vite", "cra", "grep"], "sources": ["next:sb98bee5b61c8", "vite:s3fc0892e0f58", "cra:scfb24624addd", "grep:s5170317ddea8", "grep:s057470125f4c", "grep:sb18b36e247d7", "grep:s0c4ddbee9e68"], "status": "REASONED", "verify": [1]}, + "verify-backup": {"text": "Known previously deployed backup URLs should return 403 or 404 without the file contents.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "verify-suffixes": {"text": "GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution.", "components": ["nginx", "apache", "caddy", "coreutils", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b", "coreutils:sabbd081d1d5a"], "status": "REASONED", "verify": [2]} + } +} +```` + diff --git a/python.md b/python.md index 8d872c74e..09f7ce969 100644 --- a/python.md +++ b/python.md @@ -1,10 +1,268 @@ ---- -version_basis: { +# Python web apps: TLS and authentication + +Written against Django documentation 6.1, Python http.server documentation 3.14.4, Python http.server source v3.14.4, Python standard library documentation 3.14, CPython ssl source and documentation v3.14.8, CPython secrets documentation and source v3.14.8, PyOTP v2.9.0, and qrcode v8.2, plus 6 more recorded bases; 5 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 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. + +Covers Flask, FastAPI/Uvicorn, Gunicorn, and Django. This guide's preferred production layout: bind the app server to `127.0.0.1` and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). For managed HTTP ingress, bind the listener required by your deployment; TLS termination depends on the platform and configuration described below ([paas.md](paas.md)). The app servers can also terminate TLS themselves, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). + +## 1. TLS per server + +Flask's built-in server (development only; it is not a production server, TLS or not): + +```python +app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem")) +# ssl_context="adhoc" generates a throwaway self-signed cert; requires the cryptography package +``` + +Gunicorn (Flask/Django/WSGI in production): + +```bash +gunicorn --bind 0.0.0.0:8443 \ + --certfile /etc/ssl/certs/server.crt \ + --keyfile /etc/ssl/private/server.key \ + app:app +``` + +Uvicorn (FastAPI/ASGI): + +```bash +uvicorn main:app --host 0.0.0.0 --port 8443 \ + --ssl-certfile /etc/ssl/certs/server.crt \ + --ssl-keyfile /etc/ssl/private/server.key +``` + +This guide's advice: Bind to `0.0.0.0` only when either (a) the process itself terminates TLS with authentication in place, or (b) a platform terminates TLS at its ingress and reaches your app over a private or container network. Otherwise, on a host you expose directly, keep `127.0.0.1` and terminate TLS in a reverse proxy. + +This guide's advice for managed platforms: do not add extra listeners; bind only the listener required by your deployment. The documented HTTP ingress mappings are: + +- Render web services must bind `0.0.0.0`. Render recommends the `PORT` environment variable (at the time of writing, default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound ([web services](https://render.com/docs/web-services)). +- Heroku assigns each web process its port through `PORT` ([runtime principles](https://devcenter.heroku.com/articles/runtime-principles)). Fir Private Spaces networking uses IPv6 by default; Heroku warns that IPv4-only assumptions do not bind properly and recommends an IPv6 or dual-stack listener, typically `::` ([generations](https://devcenter.heroku.com/articles/generations)). These pages do not establish Cedar's listener address. +- Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the configured `internal_port` to the app listener ([service binding](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42)). IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime ([wildcard qualification](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92)). +- Railway domains route to a detected or selected internal target port; the cited passage does not establish a required listener address, an injected `PORT` or the TLS termination location ([target ports](https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146)). + +Render's load balancer terminates inbound HTTPS and forwards HTTP to the web service ([web services](https://render.com/docs/web-services)). Heroku's HTTP routing page describes a load balancer offering SSL termination and routers forwarding HTTP to web dynos; it notes that both Private Spaces generations have different routers, so this is not a complete account of Private Spaces routing ([HTTP routing](https://devcenter.heroku.com/articles/http-routing)). Fly Proxy's `tls` handler terminates TLS and forwards plaintext ([TLS handler](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176)); without handlers it passes TCP through, allowing the app to terminate TLS itself ([TCP passthrough](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97)). This guide's advice: plain HTTP is acceptable only behind protected ingress that terminates TLS; application authentication is still required ([paas.md](paas.md)). + +## 2. Django settings for HTTPS + +```python +SECURE_SSL_REDIRECT = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # only behind a proxy that STRIPS the client's value and sets its own +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_HSTS_SECONDS = 3600 # start small; raise to 31536000 (1 year) after HTTPS is confirmed working +SECURE_HSTS_INCLUDE_SUBDOMAINS = False # True ONLY after every subdomain also serves valid HTTPS; the commitment is irreversible for SECURE_HSTS_SECONDS +``` + +`SECURE_PROXY_SSL_HEADER` must be set only when a proxy you control, or a managed platform that reliably guarantees this, strips any client-supplied `X-Forwarded-Proto` from every request and sets it itself from the real connection scheme; otherwise a client can spoof it and make Django treat plain HTTP as HTTPS. Leave it unset if you are not behind such a proxy, and confirm the app is reachable only through that proxy, not directly. Run `python manage.py check --deploy` and fix what it reports. + +## 3. Authentication + +Follow [authentication.md](authentication.md). Framework specifics: + +- Keep Django's built-in password hashing; do not replace it with custom code (this guide's advice). +- If your Flask or FastAPI application implements its own password accounts, provide its account storage and hash passwords with `argon2-cffi` or `bcrypt` (this guide's advice; follow the algorithm and legacy-bcrypt constraints in [authentication.md](authentication.md)): + +```python +from argon2 import PasswordHasher +ph = PasswordHasher() +hash_ = ph.hash(password) +ph.verify(hash_, password) # raises on mismatch +``` + +The [argon2-cffi 25.1.0 example](https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28) documents these `PasswordHasher`, `hash` and `verify` calls and a `VerifyMismatchError` on mismatch. The [pinned bcrypt example](https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76) documents `hashpw(password, gensalt())` and `checkpw(password, hashed)`; these API examples do not establish framework user-store capabilities. + +- Generate tokens and secrets with the standard library, and load them from the environment (this guide's advice): + +```python +import secrets +token = secrets.token_urlsafe(32) +``` + +In [CPython v3.14.8](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103), `token_urlsafe(nbytes=None)` returns URL-safe Base64 text from `nbytes` random bytes; the argument counts input bytes, not output characters. The explicit `32` above is this guide's example choice, not a size suitable for every secret. If the argument is omitted or `None`, it uses `DEFAULT_ENTROPY`, [32 bytes at this pin](https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45); the [documented default can change](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130), including in maintenance releases. + +- FastAPI's `fastapi.security` classes (`HTTPBearer`, `APIKeyHeader`, `OAuth2AuthorizationCodeBearer`, and so on) extract the credential from the request and declare the OpenAPI security scheme; what they check varies by helper (an API-key helper checks only that a value is present; the HTTP and OAuth2 bearer helpers may also check the scheme), but none authenticate the credential (no signature, issuer, audience, or expiry check), and `OpenIdConnect` is documented as a stub that does not implement the scheme or use the discovery URL. Use them to extract the token, then validate it (signature, issuer, audience, expiry) with an OIDC library such as Authlib, and authorize per [oidc-integration.md](oidc-integration.md). +- Rate-limit login routes (this guide's advice; for example with a proxy-level limit or a library such as slowapi for Starlette/FastAPI ASGI apps' HTTP endpoints; endpoints using slowapi's `limit` decorator must take an explicit `request` argument, and slowapi does not support WebSocket endpoints). +- MFA: add TOTP with [pyotp](https://github.com/pyauth/pyotp) plus the [qrcode](https://pypi.org/project/qrcode/) package for enrolment QR codes; [django-otp](https://pypi.org/project/django-otp/) integrates this into Django. Requirements and options in [mfa.md](mfa.md). + +## 4. Client-side TLS discipline + +Never ship `verify=False` (requests/httpx) or `ssl._create_unverified_context` (this guide's advice). For an internal CA, point the client at it instead (this guide's advice): + +```bash +export REQUESTS_CA_BUNDLE=/path/ca.crt # requests +export SSL_CERT_FILE=/path/ca.crt # HTTPX with verify=True and trust_env=True; OpenSSL default CA file for ssl when its default verify paths apply (see caveat below) +``` + +HTTPX 0.28.1's SSL guide says `SSL_CERT_FILE` and `SSL_CERT_DIR` are not automatically used and shows an explicit context; its code path contradicts that: its sync and async transports pass `verify` and `trust_env` to `create_ssl_context`, which reads `SSL_CERT_FILE` when both are true. Its environment guide says environment variables are used by default but does not list `SSL_CERT_FILE`. This guide's advice: use the export above for that HTTPX code path; pass an explicit CA context for other configurations. With no CA specified and verification enabled, CPython's `ssl.create_default_context()` tries to load default CAs (CPython notes this may fail silently). `ssl.get_default_verify_paths()` reports OpenSSL's environment key for a CA file; OpenSSL names `SSL_CERT_FILE` as an override for its default CA file. CPython's cited files do not name `SSL_CERT_FILE`, so whether a given `ssl` configuration uses that override remains unverified. + +## 5. Do not quick-share files with `http.server` + +`python -m http.server` is a common quick-share suggestion, and it is the wrong one on any reachable host. By default it binds every interface (as of Python 3.14.4), and it serves the current directory: source, `.env`, private keys, and database dumps are all downloadable by anyone who can reach the port. It also follows symbolic links, so a link in that directory hands out files from outside it, and the Python docs mark the module as not suitable for production. If you have no alternative, bind loopback and serve a directory that holds only what you mean to share: + +```bash +cd /path/to/a/directory/with/nothing/private || exit +python -m http.server -b 127.0.0.1 8000 +``` + +Reach it through an SSH tunnel, including one that runs over your tailnet, never a public bind. + +## 6. Verify (REASONED: TLS, authentication and listener expectations follow the Sources below and linked deployment guidance; no deployment run or outcome is recorded in this guide. This metadata-only review has no deployed Python application, proxy or managed ingress to test.) + +```bash +curl -q -sSI --noproxy '*' https://example.com/ # public HTTPS endpoint: must succeed without -k; if not, investigate the cert, chain, hostname, or your client trust store, never -k +# An unauthenticated request to a real protected path must be REJECTED and a valid credential ACCEPTED. --noproxy +# disables YOUR client proxy, not a reverse proxy in front of the app, so if the app sits behind an auth proxy this +# pair cannot tell whether the app or the proxy enforced it: confirm the layer by probing the app directly or in its logs. +curl -q -g -sS --noproxy '*' -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # no credentials: expect 401/403 +curl -q -g -sS --noproxy '*' -H @cred.txt -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # valid credential from a file (cred.txt holds a full header line, e.g. "Authorization: Bearer ..."): expect 2xx +ss -tlnp # every listener: loopback for a same-host reverse proxy, or the platform's required address and port from section 1 on managed ingress +``` + +## Sources (checked September 2026) + +- Python `http.server` (binds all interfaces by default, serves the current directory, follows symlinks, not for production) (Python 3.14.4): https://docs.python.org/3/library/http.server.html +- Gunicorn documentation (settings reference: bind, certfile, keyfile, ca_certs): https://gunicorn.org/reference/settings/ +- Uvicorn settings reference: https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md +- Django deployment checklist: https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ +- argon2-cffi: https://argon2-cffi.readthedocs.io/ +- argon2-cffi PasswordHasher, hash and verify mismatch example (25.1.0, checked October 2026): https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28 +- Python bcrypt hashpw, gensalt and checkpw example (commit 2b4ba9ac84df972e8e81311d09559af8dc82ef33, checked October 2026): https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76 +- Werkzeug serving (`ssl_context="adhoc"` requires cryptography): https://werkzeug.palletsprojects.com/en/stable/serving/ +- FastAPI security reference: https://fastapi.tiangolo.com/reference/security/ ; `OpenIdConnect` source (stub warning): https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py +- `http.server` binds with `bind=None` and `AI_PASSIVE`, the wildcard address (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334 +- `python -m http.server`'s `-b/--bind` has no default value, and its help says "default: all interfaces" (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373 +- Python 3.14 secrets.token_urlsafe (series documentation, checked October 2026): https://docs.python.org/3.14/library/secrets.html +- CPython token_urlsafe input bytes and Base64 output (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103 +- CPython secrets default entropy and token_bytes fallback (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45 +- CPython secrets default may change in maintenance releases (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130 +- Python 3.14 os.getenv environment loading (series documentation, checked October 2026): https://docs.python.org/3.14/library/os.html#os.getenv +- PyOTP one-time passwords and MFA (v2.9.0, checked October 2026): https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5 +- qrcode enrolment image generation (v8.2, checked October 2026): https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44 +- django-otp Django authentication integration (1.6.1, checked October 2026): https://pypi.org/project/django-otp/1.6.1/ +- Requests REQUESTS_CA_BUNDLE trust roots (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 +- slowapi Starlette/FastAPI scope (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3 +- slowapi limit decorator, endpoint request argument and WebSocket limitation (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50 +- HTTPX environment-variable default and trust_env (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7 +- HTTPX environment guide variable list, which omits SSL_CERT_FILE (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53 +- HTTPX SSL guide, verify=False example (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17 +- HTTPX SSL guide, conflicting SSL_CERT_FILE statement (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86 +- HTTPX SSL context selection with verify=True and trust_env=True (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40 +- HTTPX sync transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154 +- HTTPX async transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298 +- CPython ssl get_default_verify_paths (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405 +- CPython ssl load_default_certs (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548 +- CPython ssl create_default_context conditional default-CA load (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736 +- CPython ssl _create_unverified_context (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754 +- CPython ssl documentation for default verify paths and OpenSSL environment keys (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433 +- CPython ssl documentation for default CAs and silent failure (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649 +- OpenSSL default CA file environment override (openssl-4.0.3, checked October 2026): https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49 +- Requests verify=False disables certificate verification (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243 +- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services +- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing +- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles +- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations +- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 +- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 +- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 +- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 +- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 + + + +## 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/python.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): + +- django: Django documentation (6.1) +- python: Python http.server documentation (3.14.4) +- python-pin: Python http.server source (v3.14.4) +- stdlib-docs: Python standard library documentation (3.14) +- cpython-ssl: CPython ssl source and documentation (v3.14.8) +- cpython-secrets: CPython secrets documentation and source (v3.14.8) +- gunicorn: Gunicorn documentation (unknown) +- uvicorn: Uvicorn source (5ac6265a01ff6dcadb0e4250152c3deaf8a168a9) +- fastapi: FastAPI documentation (unknown) +- oidc-stub: FastAPI OpenIdConnect source (31bbb380748ccead62fc0f42dbf4273f11dadccf) +- bcrypt-api: Python bcrypt API example (2b4ba9ac84df972e8e81311d09559af8dc82ef33) +- werkzeug: Werkzeug documentation (unknown) +- argon: argon2-cffi documentation (unknown) +- pyotp: PyOTP (v2.9.0) +- qrcode: qrcode (v8.2) +- django-otp: django-otp (1.6.1) +- requests: Requests (v2.32.3) +- slowapi: slowapi source (v0.1.10) +- httpx: HTTPX source and documentation (0.28.1) +- openssl: OpenSSL documentation (openssl-4.0.3) +- argon-api: argon2-cffi API example (25.1.0) +- render: Render documentation (unknown) +- heroku: Heroku documentation (unknown) +- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) +- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) + +Claims (full source mapping in the JSON block below): + +- bind (REASONED; gunicorn, uvicorn, render, heroku, fly-pinned, railway): This guide advises loopback for same-host TLS proxies, no extra managed listeners, and wildcard only for authenticated direct TLS or private TLS ingress; app auth remains required. Rules vary (Fir Private Spaces: IPv6/dual-stack recommended; Fly without handlers: TCP passthrough); Railway address/injected port/TLS location and Cedar address remain uncited. +- flask-tls (REASONED; werkzeug): Flask development server binds 127.0.0.1:8443 with cert/key ssl_context; adhoc requires cryptography and remains development-only. +- gunicorn-tls (REASONED; gunicorn): Gunicorn binds 0.0.0.0:8443 with --certfile and --keyfile for direct TLS. +- uvicorn-tls (REASONED; uvicorn): Uvicorn binds 0.0.0.0:8443 with --ssl-certfile and --ssl-keyfile for direct TLS. +- django-redirect (REASONED; django): Set SECURE_SSL_REDIRECT=True. +- django-proxy (REASONED; django): Set SECURE_PROXY_SSL_HEADER only behind a trusted proxy that strips and replaces X-Forwarded-Proto; prevent direct bypass. +- django-cookies (REASONED; django): Set SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE true. +- django-hsts (REASONED; django): Begin HSTS at 3600 seconds and include-subdomains false; raise duration and scope only after valid HTTPS is confirmed. +- deploy-check (REASONED; django): Run manage.py check --deploy and fix its findings. +- password (REASONED; django, argon, argon-api, bcrypt-api): Keep Django's built-in hashing; do not replace it with custom code (guide advice). For Flask/FastAPI apps implementing local password accounts, this guide advises account storage and argon2-cffi or bcrypt under authentication.md's constraints; argon2-cffi 25.1.0's PasswordHasher.verify raises VerifyMismatchError on mismatch, and these APIs do not establish framework user-store capabilities. +- secrets (REASONED; django, stdlib-docs, cpython-secrets): Generate secrets.token_urlsafe(32) and load secrets from the environment (guide advice); 32 counts random input bytes and is this guide's example choice, not a universal size. CPython v3.14.8 uses DEFAULT_ENTROPY (32 bytes) when omitted or None, but that default may change, including in maintenance releases. +- credential-extraction (REASONED; fastapi): FastAPI security helpers extract credentials and declare schemes; presence/scheme checks do not verify signature, issuer, audience or expiry. +- oidc (REASONED; fastapi, oidc-stub): OpenIdConnect is a stub and does not use its discovery URL; validate extracted tokens with an OIDC library and authorize separately. +- login-limit (REASONED; fastapi, slowapi): Rate-limiting login routes and a proxy-level limit are this guide's advice. slowapi v0.1.10 documents limits for Starlette/FastAPI HTTP endpoints; endpoints using its limit decorator must take an explicit request argument, and WebSocket endpoints are not supported. +- mfa (REASONED; django, pyotp, qrcode, django-otp): Use pyotp and qrcode, django-otp for Django, or linked MFA guidance. +- client-validation (REASONED; python, requests, httpx, cpython-ssl): Never ship requests/httpx verify=False or ssl._create_unverified_context; HTTPX documents verify=False and CPython defines _create_unverified_context. The prohibition is this guide's advice. +- client-ca (REASONED; python, requests, httpx, cpython-ssl, openssl): REQUESTS_CA_BUNDLE selects a requests CA. HTTPX 0.28.1's transport passes verify and trust_env to create_ssl_context, which reads SSL_CERT_FILE when verify=True and trust_env=True; its SSL guide contradicts that source. Its environment guide says environment variables are used by default but does not list SSL_CERT_FILE. CPython's ssl module reports OpenSSL default verify paths; with no CA specified and verification enabled, ssl.create_default_context() tries to load default CAs, which may fail silently. OpenSSL names SSL_CERT_FILE as an environment override for its default CA file; CPython's cited files do not name that variable, so whether a given ssl configuration uses it remains unverified. The export and explicit-context choices are this guide's advice. +- http-server-bind (REASONED; python-pin): As of Python 3.14.4, http.server binds every interface by default; explicitly bind 127.0.0.1:8000. +- http-server-files (REASONED; python): http.server serves the current directory and follows symlinks outside it; it is unsuitable for production. Share only a non-private directory through an SSH tunnel. +- verify-tls (REASONED; gunicorn, uvicorn): Public HTTPS must succeed without -k; certificate, chain, hostname and trust failures need investigation. +- verify-auth (REASONED; django, fastapi): A real protected route denies no credentials with 401/403 and accepts valid file-supplied credentials with 2xx; logs or direct probing distinguish app from proxy enforcement. +- verify-bind (REASONED; gunicorn, uvicorn): Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port. + +````json +{ "schema": 1, "checked": "2026-10-18", "documentation_checked": "2026-09", "body_sha256": "325223859acb920caba7e11658d3c3b87a9f7703faeab9e05b7641db8696fd48", "components": { + "django": { + "name": "Django documentation", + "basis": "6.1", + "sources": { + "s7289e1c111ae": "https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/" + } + }, "python": { "name": "Python http.server documentation", "basis": "3.14.4", @@ -12,39 +270,55 @@ version_basis: { "s8fae94406709": "https://docs.python.org/3/library/http.server.html" } }, - "gunicorn": { - "name": "Gunicorn documentation", - "basis": "unknown", + "python-pin": { + "name": "Python http.server source", + "basis": "v3.14.4", "sources": { - "s72daf362300c": "https://gunicorn.org/reference/settings/" + "s0038729960a0": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334", + "sa3111578d65d": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373" } }, - "uvicorn": { - "name": "Uvicorn source", - "basis": "5ac6265a01ff6dcadb0e4250152c3deaf8a168a9", + "stdlib-docs": { + "name": "Python standard library documentation", + "basis": "3.14", "sources": { - "s02429a4a09d9": "https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md" + "s971d128efc65": "https://docs.python.org/3.14/library/secrets.html", + "sdad376a711c3": "https://docs.python.org/3.14/library/os.html#os.getenv" } }, - "django": { - "name": "Django documentation", - "basis": "6.1", + "cpython-ssl": { + "name": "CPython ssl source and documentation", + "basis": "v3.14.8", "sources": { - "s7289e1c111ae": "https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/" + "sbac049853cda": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405", + "s6dcc06feaaaf": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548", + "s4d7f143c9456": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736", + "s4a1cd173d596": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754", + "s79d6191e12eb": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433", + "s82c72b9a971a": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649" } }, - "argon": { - "name": "argon2-cffi documentation", - "basis": "unknown", + "cpython-secrets": { + "name": "CPython secrets documentation and source", + "basis": "v3.14.8", "sources": { - "sff2b9a487e67": "https://argon2-cffi.readthedocs.io/" + "s31e1a792a6b1": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103", + "s7cc14a5ebfa0": "https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45", + "sdef0e1600064": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130" } }, - "werkzeug": { - "name": "Werkzeug documentation", + "gunicorn": { + "name": "Gunicorn documentation", "basis": "unknown", "sources": { - "sd5066c63a3ca": "https://werkzeug.palletsprojects.com/en/stable/serving/" + "s72daf362300c": "https://gunicorn.org/reference/settings/" + } + }, + "uvicorn": { + "name": "Uvicorn source", + "basis": "5ac6265a01ff6dcadb0e4250152c3deaf8a168a9", + "sources": { + "s02429a4a09d9": "https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md" } }, "fastapi": { @@ -61,20 +335,25 @@ version_basis: { "s876e3d51e7ac": "https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py" } }, - "python-pin": { - "name": "Python http.server source", - "basis": "v3.14.4", + "bcrypt-api": { + "name": "Python bcrypt API example", + "basis": "2b4ba9ac84df972e8e81311d09559af8dc82ef33", "sources": { - "s0038729960a0": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334", - "sa3111578d65d": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373" + "s340a2923b6da": "https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76" } }, - "stdlib-docs": { - "name": "Python standard library documentation", - "basis": "3.14", + "werkzeug": { + "name": "Werkzeug documentation", + "basis": "unknown", "sources": { - "s971d128efc65": "https://docs.python.org/3.14/library/secrets.html", - "sdad376a711c3": "https://docs.python.org/3.14/library/os.html#os.getenv" + "sd5066c63a3ca": "https://werkzeug.palletsprojects.com/en/stable/serving/" + } + }, + "argon": { + "name": "argon2-cffi documentation", + "basis": "unknown", + "sources": { + "sff2b9a487e67": "https://argon2-cffi.readthedocs.io/" } }, "pyotp": { @@ -121,22 +400,10 @@ version_basis: { "s52608d6c5e47": "https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7", "sd33b79237122": "https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53", "s928d85a8d13e": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17", - "s3734fbccdf40": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86", - "s30dd14bf172e": "https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40", - "s4a1f3b43f184": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154", - "sc5a61a9b2f64": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298" - } - }, - "cpython-ssl": { - "name": "CPython ssl source and documentation", - "basis": "v3.14.8", - "sources": { - "sbac049853cda": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405", - "s6dcc06feaaaf": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548", - "s4d7f143c9456": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736", - "s4a1cd173d596": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754", - "s79d6191e12eb": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433", - "s82c72b9a971a": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649" + "s3734fbccdf40": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86", + "s30dd14bf172e": "https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40", + "s4a1f3b43f184": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154", + "sc5a61a9b2f64": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298" } }, "openssl": { @@ -146,6 +413,13 @@ version_basis: { "s592eb65718de": "https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49" } }, + "argon-api": { + "name": "argon2-cffi API example", + "basis": "25.1.0", + "sources": { + "s193f86db0a79": "https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28" + } + }, "render": { "name": "Render documentation", "basis": "unknown", @@ -178,29 +452,6 @@ version_basis: { "sources": { "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146" } - }, - "argon-api": { - "name": "argon2-cffi API example", - "basis": "25.1.0", - "sources": { - "s193f86db0a79": "https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28" - } - }, - "bcrypt-api": { - "name": "Python bcrypt API example", - "basis": "2b4ba9ac84df972e8e81311d09559af8dc82ef33", - "sources": { - "s340a2923b6da": "https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76" - } - }, - "cpython-secrets": { - "name": "CPython secrets documentation and source", - "basis": "v3.14.8", - "sources": { - "s31e1a792a6b1": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103", - "s7cc14a5ebfa0": "https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45", - "sdef0e1600064": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130" - } } }, "claims": { @@ -228,230 +479,5 @@ version_basis: { "verify-bind": {"text": "Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port.", "components": ["gunicorn", "uvicorn"], "sources": ["gunicorn:s72daf362300c", "uvicorn:s02429a4a09d9"], "status": "REASONED", "verify": [1]} } } ---- -# Python web apps: 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): - -- python: Python http.server documentation (3.14.4) -- gunicorn: Gunicorn documentation (unknown) -- uvicorn: Uvicorn source (5ac6265a01ff6dcadb0e4250152c3deaf8a168a9) -- django: Django documentation (6.1) -- argon: argon2-cffi documentation (unknown) -- werkzeug: Werkzeug documentation (unknown) -- fastapi: FastAPI documentation (unknown) -- oidc-stub: FastAPI OpenIdConnect source (31bbb380748ccead62fc0f42dbf4273f11dadccf) -- python-pin: Python http.server source (v3.14.4) -- stdlib-docs: Python standard library documentation (3.14) -- pyotp: PyOTP (v2.9.0) -- qrcode: qrcode (v8.2) -- django-otp: django-otp (1.6.1) -- requests: Requests (v2.32.3) -- slowapi: slowapi source (v0.1.10) -- httpx: HTTPX source and documentation (0.28.1) -- cpython-ssl: CPython ssl source and documentation (v3.14.8) -- openssl: OpenSSL documentation (openssl-4.0.3) -- render: Render documentation (unknown) -- heroku: Heroku documentation (unknown) -- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) -- railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) -- argon-api: argon2-cffi API example (25.1.0) -- bcrypt-api: Python bcrypt API example (2b4ba9ac84df972e8e81311d09559af8dc82ef33) -- cpython-secrets: CPython secrets documentation and source (v3.14.8) - -Claims (full source mapping in the front matter): - -- bind (REASONED; gunicorn, uvicorn, render, heroku, fly-pinned, railway): This guide advises loopback for same-host TLS proxies, no extra managed listeners, and wildcard only for authenticated direct TLS or private TLS ingress; app auth remains required. Rules vary (Fir Private Spaces: IPv6/dual-stack recommended; Fly without handlers: TCP passthrough); Railway address/injected port/TLS location and Cedar address remain uncited. -- flask-tls (REASONED; werkzeug): Flask development server binds 127.0.0.1:8443 with cert/key ssl_context; adhoc requires cryptography and remains development-only. -- gunicorn-tls (REASONED; gunicorn): Gunicorn binds 0.0.0.0:8443 with --certfile and --keyfile for direct TLS. -- uvicorn-tls (REASONED; uvicorn): Uvicorn binds 0.0.0.0:8443 with --ssl-certfile and --ssl-keyfile for direct TLS. -- django-redirect (REASONED; django): Set SECURE_SSL_REDIRECT=True. -- django-proxy (REASONED; django): Set SECURE_PROXY_SSL_HEADER only behind a trusted proxy that strips and replaces X-Forwarded-Proto; prevent direct bypass. -- django-cookies (REASONED; django): Set SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE true. -- django-hsts (REASONED; django): Begin HSTS at 3600 seconds and include-subdomains false; raise duration and scope only after valid HTTPS is confirmed. -- deploy-check (REASONED; django): Run manage.py check --deploy and fix its findings. -- password (REASONED; django, argon, argon-api, bcrypt-api): Keep Django's built-in hashing; do not replace it with custom code (guide advice). For Flask/FastAPI apps implementing local password accounts, this guide advises account storage and argon2-cffi or bcrypt under authentication.md's constraints; argon2-cffi 25.1.0's PasswordHasher.verify raises VerifyMismatchError on mismatch, and these APIs do not establish framework user-store capabilities. -- secrets (REASONED; django, stdlib-docs, cpython-secrets): Generate secrets.token_urlsafe(32) and load secrets from the environment (guide advice); 32 counts random input bytes and is this guide's example choice, not a universal size. CPython v3.14.8 uses DEFAULT_ENTROPY (32 bytes) when omitted or None, but that default may change, including in maintenance releases. -- credential-extraction (REASONED; fastapi): FastAPI security helpers extract credentials and declare schemes; presence/scheme checks do not verify signature, issuer, audience or expiry. -- oidc (REASONED; fastapi, oidc-stub): OpenIdConnect is a stub and does not use its discovery URL; validate extracted tokens with an OIDC library and authorize separately. -- login-limit (REASONED; fastapi, slowapi): Rate-limiting login routes and a proxy-level limit are this guide's advice. slowapi v0.1.10 documents limits for Starlette/FastAPI HTTP endpoints; endpoints using its limit decorator must take an explicit request argument, and WebSocket endpoints are not supported. -- mfa (REASONED; django, pyotp, qrcode, django-otp): Use pyotp and qrcode, django-otp for Django, or linked MFA guidance. -- client-validation (REASONED; python, requests, httpx, cpython-ssl): Never ship requests/httpx verify=False or ssl._create_unverified_context; HTTPX documents verify=False and CPython defines _create_unverified_context. The prohibition is this guide's advice. -- client-ca (REASONED; python, requests, httpx, cpython-ssl, openssl): REQUESTS_CA_BUNDLE selects a requests CA. HTTPX 0.28.1's transport passes verify and trust_env to create_ssl_context, which reads SSL_CERT_FILE when verify=True and trust_env=True; its SSL guide contradicts that source. Its environment guide says environment variables are used by default but does not list SSL_CERT_FILE. CPython's ssl module reports OpenSSL default verify paths; with no CA specified and verification enabled, ssl.create_default_context() tries to load default CAs, which may fail silently. OpenSSL names SSL_CERT_FILE as an environment override for its default CA file; CPython's cited files do not name that variable, so whether a given ssl configuration uses it remains unverified. The export and explicit-context choices are this guide's advice. -- http-server-bind (REASONED; python-pin): As of Python 3.14.4, http.server binds every interface by default; explicitly bind 127.0.0.1:8000. -- http-server-files (REASONED; python): http.server serves the current directory and follows symlinks outside it; it is unsuitable for production. Share only a non-private directory through an SSH tunnel. -- verify-tls (REASONED; gunicorn, uvicorn): Public HTTPS must succeed without -k; certificate, chain, hostname and trust failures need investigation. -- verify-auth (REASONED; django, fastapi): A real protected route denies no credentials with 401/403 and accepts valid file-supplied credentials with 2xx; logs or direct probing distinguish app from proxy enforcement. -- verify-bind (REASONED; gunicorn, uvicorn): Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port. +```` - -Covers Flask, FastAPI/Uvicorn, Gunicorn, and Django. This guide's preferred production layout: bind the app server to `127.0.0.1` and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). For managed HTTP ingress, bind the listener required by your deployment; TLS termination depends on the platform and configuration described below ([paas.md](paas.md)). The app servers can also terminate TLS themselves, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). - -## 1. TLS per server - -Flask's built-in server (development only; it is not a production server, TLS or not): - -```python -app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem")) -# ssl_context="adhoc" generates a throwaway self-signed cert; requires the cryptography package -``` - -Gunicorn (Flask/Django/WSGI in production): - -```bash -gunicorn --bind 0.0.0.0:8443 \ - --certfile /etc/ssl/certs/server.crt \ - --keyfile /etc/ssl/private/server.key \ - app:app -``` - -Uvicorn (FastAPI/ASGI): - -```bash -uvicorn main:app --host 0.0.0.0 --port 8443 \ - --ssl-certfile /etc/ssl/certs/server.crt \ - --ssl-keyfile /etc/ssl/private/server.key -``` - -This guide's advice: Bind to `0.0.0.0` only when either (a) the process itself terminates TLS with authentication in place, or (b) a platform terminates TLS at its ingress and reaches your app over a private or container network. Otherwise, on a host you expose directly, keep `127.0.0.1` and terminate TLS in a reverse proxy. - -This guide's advice for managed platforms: do not add extra listeners; bind only the listener required by your deployment. The documented HTTP ingress mappings are: - -- Render web services must bind `0.0.0.0`. Render recommends the `PORT` environment variable (at the time of writing, default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound ([web services](https://render.com/docs/web-services)). -- Heroku assigns each web process its port through `PORT` ([runtime principles](https://devcenter.heroku.com/articles/runtime-principles)). Fir Private Spaces networking uses IPv6 by default; Heroku warns that IPv4-only assumptions do not bind properly and recommends an IPv6 or dual-stack listener, typically `::` ([generations](https://devcenter.heroku.com/articles/generations)). These pages do not establish Cedar's listener address. -- Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the configured `internal_port` to the app listener ([service binding](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42)). IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime ([wildcard qualification](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92)). -- Railway domains route to a detected or selected internal target port; the cited passage does not establish a required listener address, an injected `PORT` or the TLS termination location ([target ports](https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146)). - -Render's load balancer terminates inbound HTTPS and forwards HTTP to the web service ([web services](https://render.com/docs/web-services)). Heroku's HTTP routing page describes a load balancer offering SSL termination and routers forwarding HTTP to web dynos; it notes that both Private Spaces generations have different routers, so this is not a complete account of Private Spaces routing ([HTTP routing](https://devcenter.heroku.com/articles/http-routing)). Fly Proxy's `tls` handler terminates TLS and forwards plaintext ([TLS handler](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176)); without handlers it passes TCP through, allowing the app to terminate TLS itself ([TCP passthrough](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97)). This guide's advice: plain HTTP is acceptable only behind protected ingress that terminates TLS; application authentication is still required ([paas.md](paas.md)). - -## 2. Django settings for HTTPS - -```python -SECURE_SSL_REDIRECT = True -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # only behind a proxy that STRIPS the client's value and sets its own -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -SECURE_HSTS_SECONDS = 3600 # start small; raise to 31536000 (1 year) after HTTPS is confirmed working -SECURE_HSTS_INCLUDE_SUBDOMAINS = False # True ONLY after every subdomain also serves valid HTTPS; the commitment is irreversible for SECURE_HSTS_SECONDS -``` - -`SECURE_PROXY_SSL_HEADER` must be set only when a proxy you control, or a managed platform that reliably guarantees this, strips any client-supplied `X-Forwarded-Proto` from every request and sets it itself from the real connection scheme; otherwise a client can spoof it and make Django treat plain HTTP as HTTPS. Leave it unset if you are not behind such a proxy, and confirm the app is reachable only through that proxy, not directly. Run `python manage.py check --deploy` and fix what it reports. - -## 3. Authentication - -Follow [authentication.md](authentication.md). Framework specifics: - -- Keep Django's built-in password hashing; do not replace it with custom code (this guide's advice). -- If your Flask or FastAPI application implements its own password accounts, provide its account storage and hash passwords with `argon2-cffi` or `bcrypt` (this guide's advice; follow the algorithm and legacy-bcrypt constraints in [authentication.md](authentication.md)): - -```python -from argon2 import PasswordHasher -ph = PasswordHasher() -hash_ = ph.hash(password) -ph.verify(hash_, password) # raises on mismatch -``` - -The [argon2-cffi 25.1.0 example](https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28) documents these `PasswordHasher`, `hash` and `verify` calls and a `VerifyMismatchError` on mismatch. The [pinned bcrypt example](https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76) documents `hashpw(password, gensalt())` and `checkpw(password, hashed)`; these API examples do not establish framework user-store capabilities. - -- Generate tokens and secrets with the standard library, and load them from the environment (this guide's advice): - -```python -import secrets -token = secrets.token_urlsafe(32) -``` - -In [CPython v3.14.8](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103), `token_urlsafe(nbytes=None)` returns URL-safe Base64 text from `nbytes` random bytes; the argument counts input bytes, not output characters. The explicit `32` above is this guide's example choice, not a size suitable for every secret. If the argument is omitted or `None`, it uses `DEFAULT_ENTROPY`, [32 bytes at this pin](https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45); the [documented default can change](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130), including in maintenance releases. - -- FastAPI's `fastapi.security` classes (`HTTPBearer`, `APIKeyHeader`, `OAuth2AuthorizationCodeBearer`, and so on) extract the credential from the request and declare the OpenAPI security scheme; what they check varies by helper (an API-key helper checks only that a value is present; the HTTP and OAuth2 bearer helpers may also check the scheme), but none authenticate the credential (no signature, issuer, audience, or expiry check), and `OpenIdConnect` is documented as a stub that does not implement the scheme or use the discovery URL. Use them to extract the token, then validate it (signature, issuer, audience, expiry) with an OIDC library such as Authlib, and authorize per [oidc-integration.md](oidc-integration.md). -- Rate-limit login routes (this guide's advice; for example with a proxy-level limit or a library such as slowapi for Starlette/FastAPI ASGI apps' HTTP endpoints; endpoints using slowapi's `limit` decorator must take an explicit `request` argument, and slowapi does not support WebSocket endpoints). -- MFA: add TOTP with [pyotp](https://github.com/pyauth/pyotp) plus the [qrcode](https://pypi.org/project/qrcode/) package for enrolment QR codes; [django-otp](https://pypi.org/project/django-otp/) integrates this into Django. Requirements and options in [mfa.md](mfa.md). - -## 4. Client-side TLS discipline - -Never ship `verify=False` (requests/httpx) or `ssl._create_unverified_context` (this guide's advice). For an internal CA, point the client at it instead (this guide's advice): - -```bash -export REQUESTS_CA_BUNDLE=/path/ca.crt # requests -export SSL_CERT_FILE=/path/ca.crt # HTTPX with verify=True and trust_env=True; OpenSSL default CA file for ssl when its default verify paths apply (see caveat below) -``` - -HTTPX 0.28.1's SSL guide says `SSL_CERT_FILE` and `SSL_CERT_DIR` are not automatically used and shows an explicit context; its code path contradicts that: its sync and async transports pass `verify` and `trust_env` to `create_ssl_context`, which reads `SSL_CERT_FILE` when both are true. Its environment guide says environment variables are used by default but does not list `SSL_CERT_FILE`. This guide's advice: use the export above for that HTTPX code path; pass an explicit CA context for other configurations. With no CA specified and verification enabled, CPython's `ssl.create_default_context()` tries to load default CAs (CPython notes this may fail silently). `ssl.get_default_verify_paths()` reports OpenSSL's environment key for a CA file; OpenSSL names `SSL_CERT_FILE` as an override for its default CA file. CPython's cited files do not name `SSL_CERT_FILE`, so whether a given `ssl` configuration uses that override remains unverified. - -## 5. Do not quick-share files with `http.server` - -`python -m http.server` is a common quick-share suggestion, and it is the wrong one on any reachable host. By default it binds every interface (as of Python 3.14.4), and it serves the current directory: source, `.env`, private keys, and database dumps are all downloadable by anyone who can reach the port. It also follows symbolic links, so a link in that directory hands out files from outside it, and the Python docs mark the module as not suitable for production. If you have no alternative, bind loopback and serve a directory that holds only what you mean to share: - -```bash -cd /path/to/a/directory/with/nothing/private || exit -python -m http.server -b 127.0.0.1 8000 -``` - -Reach it through an SSH tunnel, including one that runs over your tailnet, never a public bind. - -## 6. Verify (REASONED: TLS, authentication and listener expectations follow the Sources below and linked deployment guidance; no deployment run or outcome is recorded in this guide. This metadata-only review has no deployed Python application, proxy or managed ingress to test.) - -```bash -curl -q -sSI --noproxy '*' https://example.com/ # public HTTPS endpoint: must succeed without -k; if not, investigate the cert, chain, hostname, or your client trust store, never -k -# An unauthenticated request to a real protected path must be REJECTED and a valid credential ACCEPTED. --noproxy -# disables YOUR client proxy, not a reverse proxy in front of the app, so if the app sits behind an auth proxy this -# pair cannot tell whether the app or the proxy enforced it: confirm the layer by probing the app directly or in its logs. -curl -q -g -sS --noproxy '*' -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # no credentials: expect 401/403 -curl -q -g -sS --noproxy '*' -H @cred.txt -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # valid credential from a file (cred.txt holds a full header line, e.g. "Authorization: Bearer ..."): expect 2xx -ss -tlnp # every listener: loopback for a same-host reverse proxy, or the platform's required address and port from section 1 on managed ingress -``` - -## Sources (checked September 2026) - -- Python `http.server` (binds all interfaces by default, serves the current directory, follows symlinks, not for production) (Python 3.14.4): https://docs.python.org/3/library/http.server.html -- Gunicorn documentation (settings reference: bind, certfile, keyfile, ca_certs): https://gunicorn.org/reference/settings/ -- Uvicorn settings reference: https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md -- Django deployment checklist: https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ -- argon2-cffi: https://argon2-cffi.readthedocs.io/ -- argon2-cffi PasswordHasher, hash and verify mismatch example (25.1.0, checked October 2026): https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28 -- Python bcrypt hashpw, gensalt and checkpw example (commit 2b4ba9ac84df972e8e81311d09559af8dc82ef33, checked October 2026): https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76 -- Werkzeug serving (`ssl_context="adhoc"` requires cryptography): https://werkzeug.palletsprojects.com/en/stable/serving/ -- FastAPI security reference: https://fastapi.tiangolo.com/reference/security/ ; `OpenIdConnect` source (stub warning): https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py -- `http.server` binds with `bind=None` and `AI_PASSIVE`, the wildcard address (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334 -- `python -m http.server`'s `-b/--bind` has no default value, and its help says "default: all interfaces" (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373 -- Python 3.14 secrets.token_urlsafe (series documentation, checked October 2026): https://docs.python.org/3.14/library/secrets.html -- CPython token_urlsafe input bytes and Base64 output (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103 -- CPython secrets default entropy and token_bytes fallback (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45 -- CPython secrets default may change in maintenance releases (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130 -- Python 3.14 os.getenv environment loading (series documentation, checked October 2026): https://docs.python.org/3.14/library/os.html#os.getenv -- PyOTP one-time passwords and MFA (v2.9.0, checked October 2026): https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5 -- qrcode enrolment image generation (v8.2, checked October 2026): https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44 -- django-otp Django authentication integration (1.6.1, checked October 2026): https://pypi.org/project/django-otp/1.6.1/ -- Requests REQUESTS_CA_BUNDLE trust roots (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 -- slowapi Starlette/FastAPI scope (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3 -- slowapi limit decorator, endpoint request argument and WebSocket limitation (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50 -- HTTPX environment-variable default and trust_env (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7 -- HTTPX environment guide variable list, which omits SSL_CERT_FILE (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53 -- HTTPX SSL guide, verify=False example (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17 -- HTTPX SSL guide, conflicting SSL_CERT_FILE statement (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86 -- HTTPX SSL context selection with verify=True and trust_env=True (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40 -- HTTPX sync transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154 -- HTTPX async transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298 -- CPython ssl get_default_verify_paths (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405 -- CPython ssl load_default_certs (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548 -- CPython ssl create_default_context conditional default-CA load (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736 -- CPython ssl _create_unverified_context (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754 -- CPython ssl documentation for default verify paths and OpenSSL environment keys (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433 -- CPython ssl documentation for default CAs and silent failure (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649 -- OpenSSL default CA file environment override (openssl-4.0.3, checked October 2026): https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49 -- Requests verify=False disables certificate verification (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243 -- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services -- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing -- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles -- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations -- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 -- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 -- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 -- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 -- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 diff --git a/ruby.md b/ruby.md index a7f79c2e2..5fc745b6d 100644 --- a/ruby.md +++ b/ruby.md @@ -1,177 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-26", - "documentation_checked": "2026-09", - "body_sha256": "d7d88bbf754652ad64cd1044824c2cc41633732451a815523e2adabc25c7a2a4", - "components": { - "puma-readme": { - "name": "Puma README", - "basis": "aef89221e4d729c3133c723844382331ba3bbbd9", - "sources": { - "se5323687b58f": "https://github.com/puma/puma/blob/aef89221e4d729c3133c723844382331ba3bbbd9/README.md" - } - }, - "puma-dsl": { - "name": "Puma DSL", - "basis": "d70de8b4e926f1f5fa0269dc46cdfadf52562628", - "sources": { - "s8d510226544c": "https://github.com/puma/puma/blob/d70de8b4e926f1f5fa0269dc46cdfadf52562628/lib/puma/dsl.rb" - } - }, - "puma": { - "name": "Puma defaults", - "basis": "v8.0.2", - "sources": { - "s9687194339f3": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L369-L384", - "s8e2a34b121ba": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L174", - "s1d9011be835b": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/const.rb#L214-L215" - } - }, - "rails": { - "name": "Rails documentation", - "basis": "unknown", - "sources": { - "s0e5c35fd756d": "https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html", - "s22fb0dcbb6e1": "https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html", - "sba5f72c7a627": "https://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html", - "sae7bf5d75809": "https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html", - "s574629d873e4": "https://guides.rubyonrails.org/configuring.html", - "s2abb8f0ec8d2": "https://guides.rubyonrails.org/security.html" - } - }, - "assume": { - "name": "Action Pack", - "basis": "7.1", - "sources": { - "sf720dd92b2e7": "https://github.com/rails/rails/blob/ffcbf6f205363f8c2fb3e9834bc86690dd59f1cb/actionpack/CHANGELOG.md" - } - }, - "credentials": { - "name": "Rails credentials source", - "basis": "e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a", - "sources": { - "saab0a90edc0c": "https://github.com/rails/rails/blob/e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a/railties/lib/rails/commands/credentials/USAGE" - } - }, - "rack": { - "name": "Rack::Attack", - "basis": "unknown", - "sources": { - "s1e5ca83efb79": "https://github.com/rack/rack-attack" - } - }, - "sidekiq": { - "name": "Sidekiq Web", - "basis": "unknown", - "sources": { - "s106d0b1ced6e": "https://github.com/sidekiq/sidekiq/wiki/Monitoring" - } - }, - "net-http": { - "name": "Net::HTTP source", - "basis": "39cf5f648a9e5e25312909a2dc26681edaa2402c", - "sources": { - "sbcafa3e06a73": "https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb" - } - }, - "openssl-ruby": { - "name": "Ruby OpenSSL source", - "basis": "a77ed4b9908e179cea8f018f7e245535301746a8", - "sources": { - "sf0192683941c": "https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb" - } - }, - "openssl": { - "name": "OpenSSL documentation", - "basis": "unknown", - "sources": { - "se3858048f9d6": "https://docs.openssl.org/master/man7/openssl-env/" - } - } - }, - "claims": { - "puma-default": {"text": "Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0.", "components": ["puma"], "sources": ["puma:s9687194339f3", "puma:s8e2a34b121ba", "puma:s1d9011be835b"], "status": "REASONED"}, - "puma-private": {"text": "Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED"}, - "puma-tls": {"text": "Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates.", "components": ["puma-dsl"], "sources": ["puma-dsl:s8d510226544c"], "status": "REASONED"}, - "force-ssl": {"text": "force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED"}, - "assume-ssl": {"text": "Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP.", "components": ["rails", "assume"], "sources": ["rails:s574629d873e4", "assume:sf720dd92b2e7"], "status": "REASONED"}, - "hosts": {"text": "Add the application hostname to config.hosts for Host allowlisting.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, - "session": {"text": "CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry.", "components": ["rails"], "sources": ["rails:sba5f72c7a627", "rails:s574629d873e4"], "status": "REASONED"}, - "trusted-proxies": {"text": "Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits.", "components": ["rails"], "sources": ["rails:s22fb0dcbb6e1"], "status": "REASONED"}, - "same-site-default": {"text": "cookies_same_site_protection defaults lax from load_defaults 6.1.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, - "auth-generator": {"text": "Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, - "passwords": {"text": "has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length.", "components": ["rails"], "sources": ["rails:sae7bf5d75809"], "status": "REASONED"}, - "rate-limit": {"text": "Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store.", "components": ["rails"], "sources": ["rails:s0e5c35fd756d"], "status": "REASONED"}, - "rack-attack": {"text": "Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware.", "components": ["rack"], "sources": ["rack:s1e5ca83efb79"], "status": "REASONED"}, - "credentials": {"text": "Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores.", "components": ["credentials", "rails"], "sources": ["credentials:saab0a90edc0c", "rails:s2abb8f0ec8d2"], "status": "REASONED"}, - "require-key": {"text": "require_master_key=true refuses boot without the decryption key.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, - "mfa": {"text": "Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, - "sidekiq-open": {"text": "Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, - "sidekiq-devise": {"text": "Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, - "sidekiq-basic": {"text": "Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, - "client-tls": {"text": "Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE.", "components": ["net-http"], "sources": ["net-http:sbcafa3e06a73"], "status": "REASONED"}, - "client-store": {"text": "Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store.", "components": ["openssl-ruby", "openssl"], "sources": ["openssl-ruby:sf0192683941c", "openssl:se3858048f9d6"], "status": "REASONED"}, - "verify-bind": {"text": "Inventory every listener; Puma should bind only 127.0.0.1:3000.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED", "verify": [1]}, - "verify-https": {"text": "HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED", "verify": [1]}, - "verify-dashboard": {"text": "Anonymous dashboard request should return login 302 or 401.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED", "verify": [1]}, - "verify-sidekiq": {"text": "Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED", "verify": [1]}, - "verify-key": {"text": "git ls-files config/master.key should print nothing.", "components": ["credentials"], "sources": ["credentials:saab0a90edc0c"], "status": "REASONED", "verify": [1]} - } -} ---- # Ruby on Rails and Puma: 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-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): - -- puma-readme: Puma README (aef89221e4d729c3133c723844382331ba3bbbd9) -- puma-dsl: Puma DSL (d70de8b4e926f1f5fa0269dc46cdfadf52562628) -- puma: Puma defaults (v8.0.2) -- rails: Rails documentation (unknown) -- assume: Action Pack (7.1) -- credentials: Rails credentials source (e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a) -- rack: Rack::Attack (unknown) -- sidekiq: Sidekiq Web (unknown) -- net-http: Net::HTTP source (39cf5f648a9e5e25312909a2dc26681edaa2402c) -- openssl-ruby: Ruby OpenSSL source (a77ed4b9908e179cea8f018f7e245535301746a8) -- openssl: OpenSSL documentation (unknown) - -Claims (full source mapping in the front matter): +Written against Rails documentation (no recorded version), Action Pack 7.1, and Puma defaults v8.0.2; 5 further sources are pinned to a commit and 3 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 26 claims, all reasoned from cited sources. -- puma-default (REASONED; puma): Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0. -- puma-private (REASONED; puma-readme, puma-dsl): Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only. -- puma-tls (REASONED; puma-dsl): Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates. -- force-ssl (REASONED; rails): force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS. -- assume-ssl (REASONED; rails, assume): Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP. -- hosts (REASONED; rails): Add the application hostname to config.hosts for Host allowlisting. -- session (REASONED; rails): CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry. -- trusted-proxies (REASONED; rails): Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits. -- same-site-default (REASONED; rails): cookies_same_site_protection defaults lax from load_defaults 6.1. -- auth-generator (REASONED; rails): Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password. -- passwords (REASONED; rails): has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length. -- rate-limit (REASONED; rails): Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store. -- rack-attack (REASONED; rack): Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware. -- credentials (REASONED; credentials, rails): Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores. -- require-key (REASONED; rails): require_master_key=true refuses boot without the decryption key. -- mfa (REASONED; rails): Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance. -- sidekiq-open (REASONED; sidekiq): Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues. -- sidekiq-devise (REASONED; sidekiq): Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth. -- sidekiq-basic (REASONED; sidekiq): Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &. -- client-tls (REASONED; net-http): Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE. -- client-store (REASONED; openssl-ruby, openssl): Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store. -- verify-bind (REASONED; puma-readme, puma-dsl): Inventory every listener; Puma should bind only 127.0.0.1:3000. -- verify-https (REASONED; rails): HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies. -- verify-dashboard (REASONED; rails): Anonymous dashboard request should return login 302 or 401. -- verify-sidekiq (REASONED; sidekiq): Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success. -- verify-key (REASONED; credentials): git ls-files config/master.key should print nothing. - +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. Puma's default bind is `tcp://[::]:9292` when the host has a non-loopback IPv6 address, otherwise `tcp://0.0.0.0:9292` (as of v8.0.2): every interface, plain HTTP. Rails encrypts its session cookie but still sends it in clear unless HTTPS is enforced. Preferred layout: bind Puma to loopback and terminate TLS in a reverse proxy ([nginx.md](nginx.md), [caddy.md](caddy.md), [apache.md](apache.md)) or behind [cloudflare.md](cloudflare.md), with a certificate from [free-certificates.md](free-certificates.md). Puma can also terminate TLS itself, shown below. @@ -298,3 +129,198 @@ git ls-files config/master.key # prints nothin - Net::HTTP source (`verify_mode`, `ca_file`, `VERIFY_PEER` default): https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb - Ruby OpenSSL `SSLContext` defaults (`DEFAULT_CERT_STORE.set_default_paths`, `VERIFY_PEER`): https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb - OpenSSL environment variables (`SSL_CERT_FILE`, `SSL_CERT_DIR`): https://docs.openssl.org/master/man7/openssl-env/ + + + +## 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/ruby.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): + +- rails: Rails documentation (unknown) +- assume: Action Pack (7.1) +- puma: Puma defaults (v8.0.2) +- puma-readme: Puma README (aef89221e4d729c3133c723844382331ba3bbbd9) +- puma-dsl: Puma DSL (d70de8b4e926f1f5fa0269dc46cdfadf52562628) +- credentials: Rails credentials source (e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a) +- rack: Rack::Attack (unknown) +- sidekiq: Sidekiq Web (unknown) +- net-http: Net::HTTP source (39cf5f648a9e5e25312909a2dc26681edaa2402c) +- openssl-ruby: Ruby OpenSSL source (a77ed4b9908e179cea8f018f7e245535301746a8) +- openssl: OpenSSL documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- puma-default (REASONED; puma): Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0. +- puma-private (REASONED; puma-readme, puma-dsl): Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only. +- puma-tls (REASONED; puma-dsl): Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates. +- force-ssl (REASONED; rails): force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS. +- assume-ssl (REASONED; rails, assume): Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP. +- hosts (REASONED; rails): Add the application hostname to config.hosts for Host allowlisting. +- session (REASONED; rails): CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry. +- trusted-proxies (REASONED; rails): Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits. +- same-site-default (REASONED; rails): cookies_same_site_protection defaults lax from load_defaults 6.1. +- auth-generator (REASONED; rails): Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password. +- passwords (REASONED; rails): has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length. +- rate-limit (REASONED; rails): Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store. +- rack-attack (REASONED; rack): Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware. +- credentials (REASONED; credentials, rails): Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores. +- require-key (REASONED; rails): require_master_key=true refuses boot without the decryption key. +- mfa (REASONED; rails): Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance. +- sidekiq-open (REASONED; sidekiq): Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues. +- sidekiq-devise (REASONED; sidekiq): Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth. +- sidekiq-basic (REASONED; sidekiq): Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &. +- client-tls (REASONED; net-http): Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE. +- client-store (REASONED; openssl-ruby, openssl): Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store. +- verify-bind (REASONED; puma-readme, puma-dsl): Inventory every listener; Puma should bind only 127.0.0.1:3000. +- verify-https (REASONED; rails): HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies. +- verify-dashboard (REASONED; rails): Anonymous dashboard request should return login 302 or 401. +- verify-sidekiq (REASONED; sidekiq): Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success. +- verify-key (REASONED; credentials): git ls-files config/master.key should print nothing. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "d7d88bbf754652ad64cd1044824c2cc41633732451a815523e2adabc25c7a2a4", + "components": { + "rails": { + "name": "Rails documentation", + "basis": "unknown", + "sources": { + "s0e5c35fd756d": "https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html", + "s22fb0dcbb6e1": "https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html", + "sba5f72c7a627": "https://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html", + "sae7bf5d75809": "https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html", + "s574629d873e4": "https://guides.rubyonrails.org/configuring.html", + "s2abb8f0ec8d2": "https://guides.rubyonrails.org/security.html" + } + }, + "assume": { + "name": "Action Pack", + "basis": "7.1", + "sources": { + "sf720dd92b2e7": "https://github.com/rails/rails/blob/ffcbf6f205363f8c2fb3e9834bc86690dd59f1cb/actionpack/CHANGELOG.md" + } + }, + "puma": { + "name": "Puma defaults", + "basis": "v8.0.2", + "sources": { + "s9687194339f3": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L369-L384", + "s8e2a34b121ba": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L174", + "s1d9011be835b": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/const.rb#L214-L215" + } + }, + "puma-readme": { + "name": "Puma README", + "basis": "aef89221e4d729c3133c723844382331ba3bbbd9", + "sources": { + "se5323687b58f": "https://github.com/puma/puma/blob/aef89221e4d729c3133c723844382331ba3bbbd9/README.md" + } + }, + "puma-dsl": { + "name": "Puma DSL", + "basis": "d70de8b4e926f1f5fa0269dc46cdfadf52562628", + "sources": { + "s8d510226544c": "https://github.com/puma/puma/blob/d70de8b4e926f1f5fa0269dc46cdfadf52562628/lib/puma/dsl.rb" + } + }, + "credentials": { + "name": "Rails credentials source", + "basis": "e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a", + "sources": { + "saab0a90edc0c": "https://github.com/rails/rails/blob/e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a/railties/lib/rails/commands/credentials/USAGE" + } + }, + "rack": { + "name": "Rack::Attack", + "basis": "unknown", + "sources": { + "s1e5ca83efb79": "https://github.com/rack/rack-attack" + } + }, + "sidekiq": { + "name": "Sidekiq Web", + "basis": "unknown", + "sources": { + "s106d0b1ced6e": "https://github.com/sidekiq/sidekiq/wiki/Monitoring" + } + }, + "net-http": { + "name": "Net::HTTP source", + "basis": "39cf5f648a9e5e25312909a2dc26681edaa2402c", + "sources": { + "sbcafa3e06a73": "https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb" + } + }, + "openssl-ruby": { + "name": "Ruby OpenSSL source", + "basis": "a77ed4b9908e179cea8f018f7e245535301746a8", + "sources": { + "sf0192683941c": "https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "unknown", + "sources": { + "se3858048f9d6": "https://docs.openssl.org/master/man7/openssl-env/" + } + } + }, + "claims": { + "puma-default": {"text": "Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0.", "components": ["puma"], "sources": ["puma:s9687194339f3", "puma:s8e2a34b121ba", "puma:s1d9011be835b"], "status": "REASONED"}, + "puma-private": {"text": "Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED"}, + "puma-tls": {"text": "Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates.", "components": ["puma-dsl"], "sources": ["puma-dsl:s8d510226544c"], "status": "REASONED"}, + "force-ssl": {"text": "force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED"}, + "assume-ssl": {"text": "Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP.", "components": ["rails", "assume"], "sources": ["rails:s574629d873e4", "assume:sf720dd92b2e7"], "status": "REASONED"}, + "hosts": {"text": "Add the application hostname to config.hosts for Host allowlisting.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "session": {"text": "CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry.", "components": ["rails"], "sources": ["rails:sba5f72c7a627", "rails:s574629d873e4"], "status": "REASONED"}, + "trusted-proxies": {"text": "Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits.", "components": ["rails"], "sources": ["rails:s22fb0dcbb6e1"], "status": "REASONED"}, + "same-site-default": {"text": "cookies_same_site_protection defaults lax from load_defaults 6.1.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "auth-generator": {"text": "Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "passwords": {"text": "has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length.", "components": ["rails"], "sources": ["rails:sae7bf5d75809"], "status": "REASONED"}, + "rate-limit": {"text": "Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store.", "components": ["rails"], "sources": ["rails:s0e5c35fd756d"], "status": "REASONED"}, + "rack-attack": {"text": "Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware.", "components": ["rack"], "sources": ["rack:s1e5ca83efb79"], "status": "REASONED"}, + "credentials": {"text": "Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores.", "components": ["credentials", "rails"], "sources": ["credentials:saab0a90edc0c", "rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "require-key": {"text": "require_master_key=true refuses boot without the decryption key.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "mfa": {"text": "Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "sidekiq-open": {"text": "Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "sidekiq-devise": {"text": "Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "sidekiq-basic": {"text": "Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "client-tls": {"text": "Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE.", "components": ["net-http"], "sources": ["net-http:sbcafa3e06a73"], "status": "REASONED"}, + "client-store": {"text": "Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store.", "components": ["openssl-ruby", "openssl"], "sources": ["openssl-ruby:sf0192683941c", "openssl:se3858048f9d6"], "status": "REASONED"}, + "verify-bind": {"text": "Inventory every listener; Puma should bind only 127.0.0.1:3000.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED", "verify": [1]}, + "verify-https": {"text": "HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED", "verify": [1]}, + "verify-dashboard": {"text": "Anonymous dashboard request should return login 302 or 401.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED", "verify": [1]}, + "verify-sidekiq": {"text": "Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED", "verify": [1]}, + "verify-key": {"text": "git ls-files config/master.key should print nothing.", "components": ["credentials"], "sources": ["credentials:saab0a90edc0c"], "status": "REASONED", "verify": [1]} + } +} +```` + diff --git a/server-admin-panels.md b/server-admin-panels.md index 339a764a3..647098f59 100644 --- a/server-admin-panels.md +++ b/server-admin-panels.md @@ -1,203 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-18", - "documentation_checked": "2026-09", - "body_sha256": "3478bd2ff9b8156ba6721d2d02631c39a01142288dfa2d17f860b649772c131f", - "components": { - "cockpit": { - "name": "Cockpit", - "basis": "368", - "sources": { - "sc42ba09017c7": "https://github.com/cockpit-project/cockpit/blob/368/src/systemd/cockpit.socket.in", - "s0c1e0db624a4": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/listen.adoc", - "sd0cb4ea3db56": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/https.adoc", - "sf157ab3a6f47": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/man/pages/cockpit.conf.5.adoc", - "s48ebdec28e10": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/authentication.adoc", - "s2432e599485f": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/privileges.adoc", - "sbdcd6e74a0c7": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.pam", - "sc20103cfb39b": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.spec" - } - }, - "systemd": { - "name": "systemd", - "basis": "v262", - "sources": { - "s0a96457c713e": "https://github.com/systemd/systemd/blob/v262/man/systemd.socket.xml" - } - }, - "webmin": { - "name": "Webmin", - "basis": "2.670", - "sources": { - "sa4f7ac343e85": "https://github.com/webmin/webmin/blob/2.670/setup.sh", - "sb8fec2baa0f2": "https://github.com/webmin/webmin/blob/2.670/miniserv.pl", - "sdd465cac1ff8": "https://github.com/webmin/webmin/blob/2.670/makedebian.pl", - "s9ecba8f15014": "https://github.com/webmin/webmin/blob/2.670/makerpm.pl", - "sdb2ef2bf9978": "https://github.com/webmin/webmin/blob/2.670/miniserv-lib.pl", - "s1a1dbd21dddd": "https://github.com/webmin/webmin/blob/2.670/shell/defaultacl", - "s2f3504ad1328": "https://github.com/webmin/webmin/blob/2.670/webmin/twofactor-funcs-lib.pl" - } - }, - "webmin-docs": { - "name": "Webmin docs", - "basis": "8ceae26c5a074053905cbcc6c0053be573633f3a", - "sources": { - "s7a91f9f23044": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/docs/Modules/webmin-configuration.md", - "s8b072a3403d1": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/security.md" - } - }, - "usermin": { - "name": "Usermin", - "basis": "2.570", - "sources": { - "s7fe74c9a3138": "https://github.com/webmin/usermin/blob/2.570/setup.sh" - } - }, - "pve-docs": { - "name": "Proxmox VE 9.2 docs", - "basis": "9.2.12", - "sources": { - "s8538bab3d3fc": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveproxy.adoc", - "sdbc681682e8f": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveum.adoc", - "sc6bbbfe64d09": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pve-firewall.adoc", - "s31f242f10eaa": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=certificate-management.adoc", - "s954445cf4e14": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=debian/changelog" - } - }, - "pve-manager": { - "name": "pve-manager", - "basis": "9.2.20", - "sources": { - "se4c11b901cf8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/API2/Nodes.pm", - "s1507ce3e40b8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/changelog", - "s67c1a8621e22": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/control", - "sb7f08e4e98ab": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/pveproxy.pm", - "s0b79b5bbd95f": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm" - } - }, - "pve-http": { - "name": "Proxmox HTTP server", - "basis": "5119ff9bec08c69584c0c98bea3edd0098179e5f", - "sources": { - "s753d88abe320": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/AnyEvent.pm", - "sb8d499c6bd8c": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=debian/changelog", - "s2f151d3facd0": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm" - } - }, - "pve-common": { - "name": "libpve-common-perl 9.1.21", - "basis": "5054082fe492429fc37574985c2ca812af9a3125", - "sources": { - "s062026297433": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=debian/changelog", - "s022a9e4640ab": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm" - } - } - }, - "claims": { - "private": {"text": "Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor.", "components": ["cockpit", "webmin-docs", "pve-docs"], "sources": ["cockpit:s0c1e0db624a4", "webmin-docs:s7a91f9f23044", "pve-docs:s8538bab3d3fc", "pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, - "cockpit-bind": {"text": "Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses.", "components": ["cockpit", "systemd"], "sources": ["cockpit:sc42ba09017c7", "systemd:s0a96457c713e"], "status": "REASONED"}, - "cockpit-dropin": {"text": "Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart.", "components": ["cockpit"], "sources": ["cockpit:s0c1e0db624a4"], "status": "REASONED"}, - "cockpit-tls": {"text": "One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation.", "components": ["cockpit"], "sources": ["cockpit:sd0cb4ea3db56", "cockpit:sf157ab3a6f47"], "status": "REASONED"}, - "cockpit-pam": {"text": "Local accounts use Cockpit PAM with normal SSH-equivalent privileges.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, - "cockpit-root": {"text": "First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable.", "components": ["cockpit"], "sources": ["cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, - "cockpit-escalation": {"text": "Users allowed sudo/polkit escalation get an immediately elevated session and terminal.", "components": ["cockpit"], "sources": ["cockpit:s2432e599485f"], "status": "REASONED"}, - "cockpit-mfa": {"text": "No built-in second factor; use PAM, Kerberos or client certificates.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, - "cockpit-rate": {"text": "MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, - "cockpit-loginto": {"text": "Set LoginTo=false to prevent unauthenticated scanning of reachable private networks.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, - "webmin-bind": {"text": "Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, - "webmin-discovery": {"text": "Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, - "webmin-access": {"text": "Set management bind and allow networks, then restart; empty allow admits all except denied clients.", "components": ["webmin", "webmin-docs"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978", "webmin-docs:s7a91f9f23044"], "status": "REASONED"}, - "webmin-tls": {"text": "Deb/RPM packages enable ssl=1 with a self-signed certificate.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdd465cac1ff8", "webmin:s9ecba8f15014"], "status": "REASONED"}, - "webmin-root": {"text": "Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:s1a1dbd21dddd"], "status": "REASONED"}, - "webmin-sudo": {"text": "Installed sudo=1 lets sudo-permitted Unix users log in effectively as root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, - "webmin-mfa": {"text": "TOTP/Authy exists but is not configured at install; enable and enrol every user.", "components": ["webmin"], "sources": ["webmin:s2f3504ad1328"], "status": "REASONED"}, - "webmin-advisories": {"text": "Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release.", "components": ["webmin-docs"], "sources": ["webmin-docs:s8b072a3403d1"], "status": "REASONED"}, - "webmin-rate": {"text": "Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, - "pve-bind": {"text": "pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab"], "status": "REASONED"}, - "pve-cluster": {"text": "The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules.", "components": ["pve-docs"], "sources": ["pve-docs:s8538bab3d3fc"], "status": "REASONED"}, - "pve-acl": {"text": "Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone.", "components": ["pve-docs", "pve-manager", "pve-http"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-http:s753d88abe320"], "status": "REASONED"}, - "pve-firewall": {"text": "Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses.", "components": ["pve-docs"], "sources": ["pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, - "pve-root": {"text": "root@pam can always log in as unconfined admin; installer sets the web-interface root password.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "pve-shell": {"text": "Node shell opens /bin/login -f root without another password.", "components": ["pve-manager"], "sources": ["pve-manager:se4c11b901cf8"], "status": "REASONED"}, - "pve-mfa": {"text": "Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "pve-lockout": {"text": "Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "pve-password-delay": {"text": "Unauthorized API responses delay three seconds; this is not password lockout.", "components": ["pve-http"], "sources": ["pve-http:s753d88abe320"], "status": "REASONED"}, - "pve-certificates": {"text": "Each cluster creates its own self-signed CA.", "components": ["pve-docs"], "sources": ["pve-docs:s31f242f10eaa"], "status": "REASONED"}, - "pve-tokens": {"text": "API tokens default to separated privileges and cannot access VM/node consoles.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, - "verify-cockpit": {"text": "Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults.", "components": ["cockpit"], "sources": ["cockpit:sc42ba09017c7", "cockpit:s0c1e0db624a4", "cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, - "verify-pve": {"text": "Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-docs:sdbc681682e8f", "pve-docs:sc6bbbfe64d09", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED"}, - "verify-external": {"text": "Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "REASONED"}, - "webmin-loopback": {"text": "Loopback bind with no listen line produced one TCP socket and no UDP.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2"], "status": "DEMONSTRATED", "evidence": "its only socket was TCP `127.0.0.1` on the test port, with no UDP socket."}, - "webmin-rate-run": {"text": "Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "the fifth wrong password was delayed past a five-second client timeout by miniserv's growing failure delay, and the right password was then refused with `403` \"Access denied for 127.0.0.1\"."}, - "verify-listeners": {"text": "Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host.", "components": ["cockpit", "webmin", "usermin", "pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED", "verify": [1]}, - "verify-webmin-parser": {"text": "Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "On each, the copy's parsed values matched Webmin 2.670's own `read_config_file` run on the same file, and the block printed the outcomes above; it reported an unreadable path as not checked.", "verify": [2]}, - "verify-tcp-loopback": {"text": "Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "DEMONSTRATED", "evidence": "loopback addresses pass, and hex spellings such as `::ffff:0:0` connected to a listener bound to 127.0.0.1", "verify": [3]} - } -} ---- # Server administration panels: Cockpit, Webmin, and Proxmox VE - -**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 Cockpit 368, Webmin 2.670, Proxmox VE 9.2 docs 9.2.12, pve-manager 9.2.20, Usermin 2.570, and systemd v262; 3 further sources are pinned to a commit. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 38 claims: 4 demonstrated by a check that ran and 34 reasoned from cited sources. -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): - -- cockpit: Cockpit (368) -- systemd: systemd (v262) -- webmin: Webmin (2.670) -- webmin-docs: Webmin docs (8ceae26c5a074053905cbcc6c0053be573633f3a) -- usermin: Usermin (2.570) -- pve-docs: Proxmox VE 9.2 docs (9.2.12) -- pve-manager: pve-manager (9.2.20) -- pve-http: Proxmox HTTP server (5119ff9bec08c69584c0c98bea3edd0098179e5f) -- pve-common: libpve-common-perl 9.1.21 (5054082fe492429fc37574985c2ca812af9a3125) - -Claims (full source mapping in the front matter): - -- private (REASONED; cockpit, webmin-docs, pve-docs): Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor. -- cockpit-bind (REASONED; cockpit, systemd): Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses. -- cockpit-dropin (REASONED; cockpit): Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart. -- cockpit-tls (REASONED; cockpit): One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation. -- cockpit-pam (REASONED; cockpit): Local accounts use Cockpit PAM with normal SSH-equivalent privileges. -- cockpit-root (REASONED; cockpit): First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable. -- cockpit-escalation (REASONED; cockpit): Users allowed sudo/polkit escalation get an immediately elevated session and terminal. -- cockpit-mfa (REASONED; cockpit): No built-in second factor; use PAM, Kerberos or client certificates. -- cockpit-rate (REASONED; cockpit): MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout. -- cockpit-loginto (REASONED; cockpit): Set LoginTo=false to prevent unauthenticated scanning of reachable private networks. -- webmin-bind (REASONED; webmin, usermin): Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses. -- webmin-discovery (REASONED; webmin, usermin): Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused. -- webmin-access (REASONED; webmin, webmin-docs): Set management bind and allow networks, then restart; empty allow admits all except denied clients. -- webmin-tls (REASONED; webmin): Deb/RPM packages enable ssl=1 with a self-signed certificate. -- webmin-root (REASONED; webmin): Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root. -- webmin-sudo (REASONED; webmin): Installed sudo=1 lets sudo-permitted Unix users log in effectively as root. -- webmin-mfa (REASONED; webmin): TOTP/Authy exists but is not configured at install; enable and enrol every user. -- webmin-advisories (REASONED; webmin-docs): Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release. -- webmin-rate (REASONED; webmin): Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1. -- pve-bind (REASONED; pve-docs, pve-manager, pve-http, pve-common): pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified. -- pve-cluster (REASONED; pve-docs): The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules. -- pve-acl (REASONED; pve-docs, pve-manager, pve-http): Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone. -- pve-firewall (REASONED; pve-docs): Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses. -- pve-root (REASONED; pve-docs): root@pam can always log in as unconfined admin; installer sets the web-interface root password. -- pve-shell (REASONED; pve-manager): Node shell opens /bin/login -f root without another password. -- pve-mfa (REASONED; pve-docs): Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor. -- pve-lockout (REASONED; pve-docs): Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts. -- pve-password-delay (REASONED; pve-http): Unauthorized API responses delay three seconds; this is not password lockout. -- pve-certificates (REASONED; pve-docs): Each cluster creates its own self-signed CA. -- pve-tokens (REASONED; pve-docs): API tokens default to separated privileges and cannot access VM/node consoles. -- verify-cockpit (REASONED; cockpit): Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults. -- verify-pve (REASONED; pve-docs, pve-manager, pve-http, pve-common): Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings. -- verify-external (REASONED; cockpit, webmin, usermin, pve-docs): Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP. -- webmin-loopback (DEMONSTRATED; webmin): Loopback bind with no listen line produced one TCP socket and no UDP. -- webmin-rate-run (DEMONSTRATED; webmin): Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403. -- verify-listeners (REASONED; cockpit, webmin, usermin, pve-docs, pve-manager, pve-http, pve-common): Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host. -- verify-webmin-parser (DEMONSTRATED; webmin): Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof. -- verify-tcp-loopback (DEMONSTRATED; cockpit, webmin, usermin, pve-docs): Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard. - +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 server administration panel is root on the host behind a login form. Cockpit gives any user who can escalate with sudo or polkit an administrative session and a terminal. Webmin's packages make the host's @@ -512,3 +317,224 @@ give the server's public address. A "connected" on a port you did not mean to ex - pve-manager 9.2.20 spiceproxy shared configuration, bind and ACLs, lines 31 to 59 (checked October 2026): https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm - libpve-http-server-perl 6.0.5 configuration reader, lines 10 to 97, at 5119ff9bec08c69584c0c98bea3edd0098179e5f (checked October 2026): https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm - libpve-common-perl 9.1.21 socket creation and reuse, lines 803 to 854, at 5054082fe492429fc37574985c2ca812af9a3125 (checked October 2026): https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm + + + +## 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/server-admin-panels.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): + +- cockpit: Cockpit (368) +- webmin: Webmin (2.670) +- pve-docs: Proxmox VE 9.2 docs (9.2.12) +- pve-manager: pve-manager (9.2.20) +- webmin-docs: Webmin docs (8ceae26c5a074053905cbcc6c0053be573633f3a) +- pve-http: Proxmox HTTP server (5119ff9bec08c69584c0c98bea3edd0098179e5f) +- pve-common: libpve-common-perl 9.1.21 (5054082fe492429fc37574985c2ca812af9a3125) +- usermin: Usermin (2.570) +- systemd: systemd (v262) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; cockpit, webmin-docs, pve-docs): Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor. +- cockpit-bind (REASONED; cockpit, systemd): Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses. +- cockpit-dropin (REASONED; cockpit): Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart. +- cockpit-tls (REASONED; cockpit): One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation. +- cockpit-pam (REASONED; cockpit): Local accounts use Cockpit PAM with normal SSH-equivalent privileges. +- cockpit-root (REASONED; cockpit): First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable. +- cockpit-escalation (REASONED; cockpit): Users allowed sudo/polkit escalation get an immediately elevated session and terminal. +- cockpit-mfa (REASONED; cockpit): No built-in second factor; use PAM, Kerberos or client certificates. +- cockpit-rate (REASONED; cockpit): MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout. +- cockpit-loginto (REASONED; cockpit): Set LoginTo=false to prevent unauthenticated scanning of reachable private networks. +- webmin-bind (REASONED; webmin, usermin): Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses. +- webmin-discovery (REASONED; webmin, usermin): Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused. +- webmin-access (REASONED; webmin, webmin-docs): Set management bind and allow networks, then restart; empty allow admits all except denied clients. +- webmin-tls (REASONED; webmin): Deb/RPM packages enable ssl=1 with a self-signed certificate. +- webmin-root (REASONED; webmin): Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root. +- webmin-sudo (REASONED; webmin): Installed sudo=1 lets sudo-permitted Unix users log in effectively as root. +- webmin-mfa (REASONED; webmin): TOTP/Authy exists but is not configured at install; enable and enrol every user. +- webmin-advisories (REASONED; webmin-docs): Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release. +- webmin-rate (REASONED; webmin): Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1. +- pve-bind (REASONED; pve-docs, pve-manager, pve-http, pve-common): pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified. +- pve-cluster (REASONED; pve-docs): The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules. +- pve-acl (REASONED; pve-docs, pve-manager, pve-http): Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone. +- pve-firewall (REASONED; pve-docs): Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses. +- pve-root (REASONED; pve-docs): root@pam can always log in as unconfined admin; installer sets the web-interface root password. +- pve-shell (REASONED; pve-manager): Node shell opens /bin/login -f root without another password. +- pve-mfa (REASONED; pve-docs): Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor. +- pve-lockout (REASONED; pve-docs): Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts. +- pve-password-delay (REASONED; pve-http): Unauthorized API responses delay three seconds; this is not password lockout. +- pve-certificates (REASONED; pve-docs): Each cluster creates its own self-signed CA. +- pve-tokens (REASONED; pve-docs): API tokens default to separated privileges and cannot access VM/node consoles. +- verify-cockpit (REASONED; cockpit): Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults. +- verify-pve (REASONED; pve-docs, pve-manager, pve-http, pve-common): Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings. +- verify-external (REASONED; cockpit, webmin, usermin, pve-docs): Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP. +- webmin-loopback (DEMONSTRATED; webmin): Loopback bind with no listen line produced one TCP socket and no UDP. +- webmin-rate-run (DEMONSTRATED; webmin): Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403. +- verify-listeners (REASONED; cockpit, webmin, usermin, pve-docs, pve-manager, pve-http, pve-common): Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host. +- verify-webmin-parser (DEMONSTRATED; webmin): Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof. +- verify-tcp-loopback (DEMONSTRATED; cockpit, webmin, usermin, pve-docs): Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "3478bd2ff9b8156ba6721d2d02631c39a01142288dfa2d17f860b649772c131f", + "components": { + "cockpit": { + "name": "Cockpit", + "basis": "368", + "sources": { + "sc42ba09017c7": "https://github.com/cockpit-project/cockpit/blob/368/src/systemd/cockpit.socket.in", + "s0c1e0db624a4": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/listen.adoc", + "sd0cb4ea3db56": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/https.adoc", + "sf157ab3a6f47": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/man/pages/cockpit.conf.5.adoc", + "s48ebdec28e10": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/authentication.adoc", + "s2432e599485f": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/privileges.adoc", + "sbdcd6e74a0c7": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.pam", + "sc20103cfb39b": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.spec" + } + }, + "webmin": { + "name": "Webmin", + "basis": "2.670", + "sources": { + "sa4f7ac343e85": "https://github.com/webmin/webmin/blob/2.670/setup.sh", + "sb8fec2baa0f2": "https://github.com/webmin/webmin/blob/2.670/miniserv.pl", + "sdd465cac1ff8": "https://github.com/webmin/webmin/blob/2.670/makedebian.pl", + "s9ecba8f15014": "https://github.com/webmin/webmin/blob/2.670/makerpm.pl", + "sdb2ef2bf9978": "https://github.com/webmin/webmin/blob/2.670/miniserv-lib.pl", + "s1a1dbd21dddd": "https://github.com/webmin/webmin/blob/2.670/shell/defaultacl", + "s2f3504ad1328": "https://github.com/webmin/webmin/blob/2.670/webmin/twofactor-funcs-lib.pl" + } + }, + "pve-docs": { + "name": "Proxmox VE 9.2 docs", + "basis": "9.2.12", + "sources": { + "s8538bab3d3fc": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveproxy.adoc", + "sdbc681682e8f": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveum.adoc", + "sc6bbbfe64d09": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pve-firewall.adoc", + "s31f242f10eaa": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=certificate-management.adoc", + "s954445cf4e14": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=debian/changelog" + } + }, + "pve-manager": { + "name": "pve-manager", + "basis": "9.2.20", + "sources": { + "se4c11b901cf8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/API2/Nodes.pm", + "s1507ce3e40b8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/changelog", + "s67c1a8621e22": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/control", + "sb7f08e4e98ab": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/pveproxy.pm", + "s0b79b5bbd95f": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm" + } + }, + "webmin-docs": { + "name": "Webmin docs", + "basis": "8ceae26c5a074053905cbcc6c0053be573633f3a", + "sources": { + "s7a91f9f23044": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/docs/Modules/webmin-configuration.md", + "s8b072a3403d1": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/security.md" + } + }, + "pve-http": { + "name": "Proxmox HTTP server", + "basis": "5119ff9bec08c69584c0c98bea3edd0098179e5f", + "sources": { + "s753d88abe320": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/AnyEvent.pm", + "sb8d499c6bd8c": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=debian/changelog", + "s2f151d3facd0": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm" + } + }, + "pve-common": { + "name": "libpve-common-perl 9.1.21", + "basis": "5054082fe492429fc37574985c2ca812af9a3125", + "sources": { + "s062026297433": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=debian/changelog", + "s022a9e4640ab": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm" + } + }, + "usermin": { + "name": "Usermin", + "basis": "2.570", + "sources": { + "s7fe74c9a3138": "https://github.com/webmin/usermin/blob/2.570/setup.sh" + } + }, + "systemd": { + "name": "systemd", + "basis": "v262", + "sources": { + "s0a96457c713e": "https://github.com/systemd/systemd/blob/v262/man/systemd.socket.xml" + } + } + }, + "claims": { + "private": {"text": "Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor.", "components": ["cockpit", "webmin-docs", "pve-docs"], "sources": ["cockpit:s0c1e0db624a4", "webmin-docs:s7a91f9f23044", "pve-docs:s8538bab3d3fc", "pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, + "cockpit-bind": {"text": "Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses.", "components": ["cockpit", "systemd"], "sources": ["cockpit:sc42ba09017c7", "systemd:s0a96457c713e"], "status": "REASONED"}, + "cockpit-dropin": {"text": "Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart.", "components": ["cockpit"], "sources": ["cockpit:s0c1e0db624a4"], "status": "REASONED"}, + "cockpit-tls": {"text": "One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation.", "components": ["cockpit"], "sources": ["cockpit:sd0cb4ea3db56", "cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "cockpit-pam": {"text": "Local accounts use Cockpit PAM with normal SSH-equivalent privileges.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, + "cockpit-root": {"text": "First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable.", "components": ["cockpit"], "sources": ["cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, + "cockpit-escalation": {"text": "Users allowed sudo/polkit escalation get an immediately elevated session and terminal.", "components": ["cockpit"], "sources": ["cockpit:s2432e599485f"], "status": "REASONED"}, + "cockpit-mfa": {"text": "No built-in second factor; use PAM, Kerberos or client certificates.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, + "cockpit-rate": {"text": "MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "cockpit-loginto": {"text": "Set LoginTo=false to prevent unauthenticated scanning of reachable private networks.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "webmin-bind": {"text": "Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, + "webmin-discovery": {"text": "Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, + "webmin-access": {"text": "Set management bind and allow networks, then restart; empty allow admits all except denied clients.", "components": ["webmin", "webmin-docs"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978", "webmin-docs:s7a91f9f23044"], "status": "REASONED"}, + "webmin-tls": {"text": "Deb/RPM packages enable ssl=1 with a self-signed certificate.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdd465cac1ff8", "webmin:s9ecba8f15014"], "status": "REASONED"}, + "webmin-root": {"text": "Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:s1a1dbd21dddd"], "status": "REASONED"}, + "webmin-sudo": {"text": "Installed sudo=1 lets sudo-permitted Unix users log in effectively as root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, + "webmin-mfa": {"text": "TOTP/Authy exists but is not configured at install; enable and enrol every user.", "components": ["webmin"], "sources": ["webmin:s2f3504ad1328"], "status": "REASONED"}, + "webmin-advisories": {"text": "Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release.", "components": ["webmin-docs"], "sources": ["webmin-docs:s8b072a3403d1"], "status": "REASONED"}, + "webmin-rate": {"text": "Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, + "pve-bind": {"text": "pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab"], "status": "REASONED"}, + "pve-cluster": {"text": "The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules.", "components": ["pve-docs"], "sources": ["pve-docs:s8538bab3d3fc"], "status": "REASONED"}, + "pve-acl": {"text": "Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone.", "components": ["pve-docs", "pve-manager", "pve-http"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-http:s753d88abe320"], "status": "REASONED"}, + "pve-firewall": {"text": "Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses.", "components": ["pve-docs"], "sources": ["pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, + "pve-root": {"text": "root@pam can always log in as unconfined admin; installer sets the web-interface root password.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-shell": {"text": "Node shell opens /bin/login -f root without another password.", "components": ["pve-manager"], "sources": ["pve-manager:se4c11b901cf8"], "status": "REASONED"}, + "pve-mfa": {"text": "Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-lockout": {"text": "Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-password-delay": {"text": "Unauthorized API responses delay three seconds; this is not password lockout.", "components": ["pve-http"], "sources": ["pve-http:s753d88abe320"], "status": "REASONED"}, + "pve-certificates": {"text": "Each cluster creates its own self-signed CA.", "components": ["pve-docs"], "sources": ["pve-docs:s31f242f10eaa"], "status": "REASONED"}, + "pve-tokens": {"text": "API tokens default to separated privileges and cannot access VM/node consoles.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "verify-cockpit": {"text": "Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults.", "components": ["cockpit"], "sources": ["cockpit:sc42ba09017c7", "cockpit:s0c1e0db624a4", "cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, + "verify-pve": {"text": "Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-docs:sdbc681682e8f", "pve-docs:sc6bbbfe64d09", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED"}, + "verify-external": {"text": "Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "REASONED"}, + "webmin-loopback": {"text": "Loopback bind with no listen line produced one TCP socket and no UDP.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2"], "status": "DEMONSTRATED", "evidence": "its only socket was TCP `127.0.0.1` on the test port, with no UDP socket."}, + "webmin-rate-run": {"text": "Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "the fifth wrong password was delayed past a five-second client timeout by miniserv's growing failure delay, and the right password was then refused with `403` \"Access denied for 127.0.0.1\"."}, + "verify-listeners": {"text": "Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host.", "components": ["cockpit", "webmin", "usermin", "pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED", "verify": [1]}, + "verify-webmin-parser": {"text": "Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "On each, the copy's parsed values matched Webmin 2.670's own `read_config_file` run on the same file, and the block printed the outcomes above; it reported an unreadable path as not checked.", "verify": [2]}, + "verify-tcp-loopback": {"text": "Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "DEMONSTRATED", "evidence": "loopback addresses pass, and hex spellings such as `::ffff:0:0` connected to a listener bound to 127.0.0.1", "verify": [3]} + } +} +```` + diff --git a/site/llms-full.txt b/site/llms-full.txt index 8395505e0..b4b7ac0a4 100644 --- a/site/llms-full.txt +++ b/site/llms-full.txt @@ -2487,7 +2487,7 @@ TLS without authentication leaves a service open to the whole internet over an e # Multi-factor authentication (MFA) -Written against TOTP standard (no recorded version), Caddy forward_auth minimum 2.5.1, google-authenticator-libpam source 1.11, MySQL 8.0 multifactor authentication 8.0, MySQL 8.4 multifactor authentication 8.4, MySQL 8.4 WebAuthn authentication 8.4, and authentik source version/2026.8.3, plus 10 more recorded bases; 14 further sources are pinned to a commit and 13 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 35 claims: 2 demonstrated by a check that ran and 33 reasoned from cited sources. +Written against TOTP standard (no recorded version), authentik source version/2026.8.3, Keycloak documentation 26.7.4, oauth2-proxy documentation v7.15.4, Caddy forward_auth minimum 2.5.1, google-authenticator-libpam source 1.11, and MySQL 8.0 multifactor authentication 8.0, plus 10 more recorded bases; 14 further sources are pinned to a commit and 13 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 35 claims: 2 demonstrated by a check that ran and 33 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. @@ -2658,6 +2658,9 @@ Components (recorded basis): - uri: Authenticator key URI format (unknown) - webauthn: WebAuthn standard (unknown) - authelia: Authelia proxy support (unknown) +- authentik-source: authentik source (version/2026.8.3) +- keycloak-source: Keycloak documentation (26.7.4) +- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) - caddy-min: Caddy forward_auth minimum (2.5.1) - duo: Duo editions (unknown) - duo-push-policy: Duo Verified Push policy (unknown) @@ -2671,9 +2674,6 @@ Components (recorded basis): - mysql: MySQL 8.0 multifactor authentication (8.0) - mysql-8-4: MySQL 8.4 multifactor authentication (8.4) - mysql-webauthn: MySQL 8.4 WebAuthn authentication (8.4) -- authentik-source: authentik source (version/2026.8.3) -- keycloak-source: Keycloak documentation (26.7.4) -- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) - entra-docs: Microsoft Entra documentation (231747abc59ae3d50c74e215c1cdd592eb980723) - owasp-docs: OWASP cheat sheets (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) - otp-source: pquerna/otp source (617f8d5f3f518bae0f5c3ec14a17ca19f2a6f870) @@ -2778,6 +2778,28 @@ Claims (full source mapping in the JSON block below): "s1409ac8fe3d9": "https://www.authelia.com/integration/proxies/support/" } }, + "authentik-source": { + "name": "authentik source", + "basis": "version/2026.8.3", + "sources": { + "s18275a7072e9": "https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/stages/authenticator_validate/stage.py#L80-L108" + } + }, + "keycloak-source": { + "name": "Keycloak documentation", + "basis": "26.7.4", + "sources": { + "s1e55f7daaa0d": "https://github.com/keycloak/keycloak/blob/26.7.4/docs/documentation/server_admin/topics/authentication/otp-policies.adoc#L2-L21" + } + }, + "oauth2-proxy-source": { + "name": "oauth2-proxy documentation", + "basis": "v7.15.4", + "sources": { + "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", + "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" + } + }, "caddy-min": { "name": "Caddy forward_auth minimum", "basis": "2.5.1", @@ -2872,28 +2894,6 @@ Claims (full source mapping in the JSON block below): "s807a77fa8872": "https://dev.mysql.com/doc/refman/8.4/en/webauthn-pluggable-authentication.html" } }, - "authentik-source": { - "name": "authentik source", - "basis": "version/2026.8.3", - "sources": { - "s18275a7072e9": "https://github.com/goauthentik/authentik/blob/version/2026.8.3/authentik/stages/authenticator_validate/stage.py#L80-L108" - } - }, - "keycloak-source": { - "name": "Keycloak documentation", - "basis": "26.7.4", - "sources": { - "s1e55f7daaa0d": "https://github.com/keycloak/keycloak/blob/26.7.4/docs/documentation/server_admin/topics/authentication/otp-policies.adoc#L2-L21" - } - }, - "oauth2-proxy-source": { - "name": "oauth2-proxy documentation", - "basis": "v7.15.4", - "sources": { - "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", - "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" - } - }, "entra-docs": { "name": "Microsoft Entra documentation", "basis": "231747abc59ae3d50c74e215c1cdd592eb980723", @@ -10834,42 +10834,231 @@ Claims (full source mapping in the JSON block below): # Python web apps: TLS and authentication +Written against Django documentation 6.1, Python http.server documentation 3.14.4, Python http.server source v3.14.4, Python standard library documentation 3.14, CPython ssl source and documentation v3.14.8, CPython secrets documentation and source v3.14.8, PyOTP v2.9.0, and qrcode v8.2, plus 6 more recorded bases; 5 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 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. + +Covers Flask, FastAPI/Uvicorn, Gunicorn, and Django. This guide's preferred production layout: bind the app server to `127.0.0.1` and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). For managed HTTP ingress, bind the listener required by your deployment; TLS termination depends on the platform and configuration described below ([paas.md](paas.md)). The app servers can also terminate TLS themselves, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). + +## 1. TLS per server + +Flask's built-in server (development only; it is not a production server, TLS or not): + +```python +app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem")) +# ssl_context="adhoc" generates a throwaway self-signed cert; requires the cryptography package +``` + +Gunicorn (Flask/Django/WSGI in production): + +```bash +gunicorn --bind 0.0.0.0:8443 \ + --certfile /etc/ssl/certs/server.crt \ + --keyfile /etc/ssl/private/server.key \ + app:app +``` + +Uvicorn (FastAPI/ASGI): + +```bash +uvicorn main:app --host 0.0.0.0 --port 8443 \ + --ssl-certfile /etc/ssl/certs/server.crt \ + --ssl-keyfile /etc/ssl/private/server.key +``` + +This guide's advice: Bind to `0.0.0.0` only when either (a) the process itself terminates TLS with authentication in place, or (b) a platform terminates TLS at its ingress and reaches your app over a private or container network. Otherwise, on a host you expose directly, keep `127.0.0.1` and terminate TLS in a reverse proxy. + +This guide's advice for managed platforms: do not add extra listeners; bind only the listener required by your deployment. The documented HTTP ingress mappings are: + +- Render web services must bind `0.0.0.0`. Render recommends the `PORT` environment variable (at the time of writing, default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound ([web services](https://render.com/docs/web-services)). +- Heroku assigns each web process its port through `PORT` ([runtime principles](https://devcenter.heroku.com/articles/runtime-principles)). Fir Private Spaces networking uses IPv6 by default; Heroku warns that IPv4-only assumptions do not bind properly and recommends an IPv6 or dual-stack listener, typically `::` ([generations](https://devcenter.heroku.com/articles/generations)). These pages do not establish Cedar's listener address. +- Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the configured `internal_port` to the app listener ([service binding](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42)). IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime ([wildcard qualification](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92)). +- Railway domains route to a detected or selected internal target port; the cited passage does not establish a required listener address, an injected `PORT` or the TLS termination location ([target ports](https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146)). + +Render's load balancer terminates inbound HTTPS and forwards HTTP to the web service ([web services](https://render.com/docs/web-services)). Heroku's HTTP routing page describes a load balancer offering SSL termination and routers forwarding HTTP to web dynos; it notes that both Private Spaces generations have different routers, so this is not a complete account of Private Spaces routing ([HTTP routing](https://devcenter.heroku.com/articles/http-routing)). Fly Proxy's `tls` handler terminates TLS and forwards plaintext ([TLS handler](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176)); without handlers it passes TCP through, allowing the app to terminate TLS itself ([TCP passthrough](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97)). This guide's advice: plain HTTP is acceptable only behind protected ingress that terminates TLS; application authentication is still required ([paas.md](paas.md)). + +## 2. Django settings for HTTPS + +```python +SECURE_SSL_REDIRECT = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # only behind a proxy that STRIPS the client's value and sets its own +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_HSTS_SECONDS = 3600 # start small; raise to 31536000 (1 year) after HTTPS is confirmed working +SECURE_HSTS_INCLUDE_SUBDOMAINS = False # True ONLY after every subdomain also serves valid HTTPS; the commitment is irreversible for SECURE_HSTS_SECONDS +``` + +`SECURE_PROXY_SSL_HEADER` must be set only when a proxy you control, or a managed platform that reliably guarantees this, strips any client-supplied `X-Forwarded-Proto` from every request and sets it itself from the real connection scheme; otherwise a client can spoof it and make Django treat plain HTTP as HTTPS. Leave it unset if you are not behind such a proxy, and confirm the app is reachable only through that proxy, not directly. Run `python manage.py check --deploy` and fix what it reports. + +## 3. Authentication + +Follow [authentication.md](authentication.md). Framework specifics: + +- Keep Django's built-in password hashing; do not replace it with custom code (this guide's advice). +- If your Flask or FastAPI application implements its own password accounts, provide its account storage and hash passwords with `argon2-cffi` or `bcrypt` (this guide's advice; follow the algorithm and legacy-bcrypt constraints in [authentication.md](authentication.md)): + +```python +from argon2 import PasswordHasher +ph = PasswordHasher() +hash_ = ph.hash(password) +ph.verify(hash_, password) # raises on mismatch +``` + +The [argon2-cffi 25.1.0 example](https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28) documents these `PasswordHasher`, `hash` and `verify` calls and a `VerifyMismatchError` on mismatch. The [pinned bcrypt example](https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76) documents `hashpw(password, gensalt())` and `checkpw(password, hashed)`; these API examples do not establish framework user-store capabilities. + +- Generate tokens and secrets with the standard library, and load them from the environment (this guide's advice): + +```python +import secrets +token = secrets.token_urlsafe(32) +``` + +In [CPython v3.14.8](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103), `token_urlsafe(nbytes=None)` returns URL-safe Base64 text from `nbytes` random bytes; the argument counts input bytes, not output characters. The explicit `32` above is this guide's example choice, not a size suitable for every secret. If the argument is omitted or `None`, it uses `DEFAULT_ENTROPY`, [32 bytes at this pin](https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45); the [documented default can change](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130), including in maintenance releases. + +- FastAPI's `fastapi.security` classes (`HTTPBearer`, `APIKeyHeader`, `OAuth2AuthorizationCodeBearer`, and so on) extract the credential from the request and declare the OpenAPI security scheme; what they check varies by helper (an API-key helper checks only that a value is present; the HTTP and OAuth2 bearer helpers may also check the scheme), but none authenticate the credential (no signature, issuer, audience, or expiry check), and `OpenIdConnect` is documented as a stub that does not implement the scheme or use the discovery URL. Use them to extract the token, then validate it (signature, issuer, audience, expiry) with an OIDC library such as Authlib, and authorize per [oidc-integration.md](oidc-integration.md). +- Rate-limit login routes (this guide's advice; for example with a proxy-level limit or a library such as slowapi for Starlette/FastAPI ASGI apps' HTTP endpoints; endpoints using slowapi's `limit` decorator must take an explicit `request` argument, and slowapi does not support WebSocket endpoints). +- MFA: add TOTP with [pyotp](https://github.com/pyauth/pyotp) plus the [qrcode](https://pypi.org/project/qrcode/) package for enrolment QR codes; [django-otp](https://pypi.org/project/django-otp/) integrates this into Django. Requirements and options in [mfa.md](mfa.md). + +## 4. Client-side TLS discipline + +Never ship `verify=False` (requests/httpx) or `ssl._create_unverified_context` (this guide's advice). For an internal CA, point the client at it instead (this guide's advice): + +```bash +export REQUESTS_CA_BUNDLE=/path/ca.crt # requests +export SSL_CERT_FILE=/path/ca.crt # HTTPX with verify=True and trust_env=True; OpenSSL default CA file for ssl when its default verify paths apply (see caveat below) +``` + +HTTPX 0.28.1's SSL guide says `SSL_CERT_FILE` and `SSL_CERT_DIR` are not automatically used and shows an explicit context; its code path contradicts that: its sync and async transports pass `verify` and `trust_env` to `create_ssl_context`, which reads `SSL_CERT_FILE` when both are true. Its environment guide says environment variables are used by default but does not list `SSL_CERT_FILE`. This guide's advice: use the export above for that HTTPX code path; pass an explicit CA context for other configurations. With no CA specified and verification enabled, CPython's `ssl.create_default_context()` tries to load default CAs (CPython notes this may fail silently). `ssl.get_default_verify_paths()` reports OpenSSL's environment key for a CA file; OpenSSL names `SSL_CERT_FILE` as an override for its default CA file. CPython's cited files do not name `SSL_CERT_FILE`, so whether a given `ssl` configuration uses that override remains unverified. + +## 5. Do not quick-share files with `http.server` + +`python -m http.server` is a common quick-share suggestion, and it is the wrong one on any reachable host. By default it binds every interface (as of Python 3.14.4), and it serves the current directory: source, `.env`, private keys, and database dumps are all downloadable by anyone who can reach the port. It also follows symbolic links, so a link in that directory hands out files from outside it, and the Python docs mark the module as not suitable for production. If you have no alternative, bind loopback and serve a directory that holds only what you mean to share: + +```bash +cd /path/to/a/directory/with/nothing/private || exit +python -m http.server -b 127.0.0.1 8000 +``` + +Reach it through an SSH tunnel, including one that runs over your tailnet, never a public bind. + +## 6. Verify (REASONED: TLS, authentication and listener expectations follow the Sources below and linked deployment guidance; no deployment run or outcome is recorded in this guide. This metadata-only review has no deployed Python application, proxy or managed ingress to test.) + +```bash +curl -q -sSI --noproxy '*' https://example.com/ # public HTTPS endpoint: must succeed without -k; if not, investigate the cert, chain, hostname, or your client trust store, never -k +# An unauthenticated request to a real protected path must be REJECTED and a valid credential ACCEPTED. --noproxy +# disables YOUR client proxy, not a reverse proxy in front of the app, so if the app sits behind an auth proxy this +# pair cannot tell whether the app or the proxy enforced it: confirm the layer by probing the app directly or in its logs. +curl -q -g -sS --noproxy '*' -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # no credentials: expect 401/403 +curl -q -g -sS --noproxy '*' -H @cred.txt -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # valid credential from a file (cred.txt holds a full header line, e.g. "Authorization: Bearer ..."): expect 2xx +ss -tlnp # every listener: loopback for a same-host reverse proxy, or the platform's required address and port from section 1 on managed ingress +``` + +## Sources (checked September 2026) + +- Python `http.server` (binds all interfaces by default, serves the current directory, follows symlinks, not for production) (Python 3.14.4): https://docs.python.org/3/library/http.server.html +- Gunicorn documentation (settings reference: bind, certfile, keyfile, ca_certs): https://gunicorn.org/reference/settings/ +- Uvicorn settings reference: https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md +- Django deployment checklist: https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ +- argon2-cffi: https://argon2-cffi.readthedocs.io/ +- argon2-cffi PasswordHasher, hash and verify mismatch example (25.1.0, checked October 2026): https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28 +- Python bcrypt hashpw, gensalt and checkpw example (commit 2b4ba9ac84df972e8e81311d09559af8dc82ef33, checked October 2026): https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76 +- Werkzeug serving (`ssl_context="adhoc"` requires cryptography): https://werkzeug.palletsprojects.com/en/stable/serving/ +- FastAPI security reference: https://fastapi.tiangolo.com/reference/security/ ; `OpenIdConnect` source (stub warning): https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py +- `http.server` binds with `bind=None` and `AI_PASSIVE`, the wildcard address (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334 +- `python -m http.server`'s `-b/--bind` has no default value, and its help says "default: all interfaces" (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373 +- Python 3.14 secrets.token_urlsafe (series documentation, checked October 2026): https://docs.python.org/3.14/library/secrets.html +- CPython token_urlsafe input bytes and Base64 output (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103 +- CPython secrets default entropy and token_bytes fallback (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45 +- CPython secrets default may change in maintenance releases (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130 +- Python 3.14 os.getenv environment loading (series documentation, checked October 2026): https://docs.python.org/3.14/library/os.html#os.getenv +- PyOTP one-time passwords and MFA (v2.9.0, checked October 2026): https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5 +- qrcode enrolment image generation (v8.2, checked October 2026): https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44 +- django-otp Django authentication integration (1.6.1, checked October 2026): https://pypi.org/project/django-otp/1.6.1/ +- Requests REQUESTS_CA_BUNDLE trust roots (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 +- slowapi Starlette/FastAPI scope (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3 +- slowapi limit decorator, endpoint request argument and WebSocket limitation (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50 +- HTTPX environment-variable default and trust_env (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7 +- HTTPX environment guide variable list, which omits SSL_CERT_FILE (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53 +- HTTPX SSL guide, verify=False example (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17 +- HTTPX SSL guide, conflicting SSL_CERT_FILE statement (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86 +- HTTPX SSL context selection with verify=True and trust_env=True (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40 +- HTTPX sync transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154 +- HTTPX async transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298 +- CPython ssl get_default_verify_paths (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405 +- CPython ssl load_default_certs (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548 +- CPython ssl create_default_context conditional default-CA load (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736 +- CPython ssl _create_unverified_context (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754 +- CPython ssl documentation for default verify paths and OpenSSL environment keys (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433 +- CPython ssl documentation for default CAs and silent failure (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649 +- OpenSSL default CA file environment override (openssl-4.0.3, checked October 2026): https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49 +- Requests verify=False disables certificate verification (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243 +- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services +- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing +- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles +- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations +- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 +- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 +- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 +- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 +- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 + + +## 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/python.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): +- django: Django documentation (6.1) - python: Python http.server documentation (3.14.4) +- python-pin: Python http.server source (v3.14.4) +- stdlib-docs: Python standard library documentation (3.14) +- cpython-ssl: CPython ssl source and documentation (v3.14.8) +- cpython-secrets: CPython secrets documentation and source (v3.14.8) - gunicorn: Gunicorn documentation (unknown) - uvicorn: Uvicorn source (5ac6265a01ff6dcadb0e4250152c3deaf8a168a9) -- django: Django documentation (6.1) -- argon: argon2-cffi documentation (unknown) -- werkzeug: Werkzeug documentation (unknown) - fastapi: FastAPI documentation (unknown) - oidc-stub: FastAPI OpenIdConnect source (31bbb380748ccead62fc0f42dbf4273f11dadccf) -- python-pin: Python http.server source (v3.14.4) -- stdlib-docs: Python standard library documentation (3.14) +- bcrypt-api: Python bcrypt API example (2b4ba9ac84df972e8e81311d09559af8dc82ef33) +- werkzeug: Werkzeug documentation (unknown) +- argon: argon2-cffi documentation (unknown) - pyotp: PyOTP (v2.9.0) - qrcode: qrcode (v8.2) - django-otp: django-otp (1.6.1) - requests: Requests (v2.32.3) - slowapi: slowapi source (v0.1.10) - httpx: HTTPX source and documentation (0.28.1) -- cpython-ssl: CPython ssl source and documentation (v3.14.8) - openssl: OpenSSL documentation (openssl-4.0.3) +- argon-api: argon2-cffi API example (25.1.0) - render: Render documentation (unknown) - heroku: Heroku documentation (unknown) - fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) - railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) -- argon-api: argon2-cffi API example (25.1.0) -- bcrypt-api: Python bcrypt API example (2b4ba9ac84df972e8e81311d09559af8dc82ef33) -- cpython-secrets: CPython secrets documentation and source (v3.14.8) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): - bind (REASONED; gunicorn, uvicorn, render, heroku, fly-pinned, railway): This guide advises loopback for same-host TLS proxies, no extra managed listeners, and wildcard only for authenticated direct TLS or private TLS ingress; app auth remains required. Rules vary (Fir Private Spaces: IPv6/dual-stack recommended; Fly without handlers: TCP passthrough); Railway address/injected port/TLS location and Cedar address remain uncited. - flask-tls (REASONED; werkzeug): Flask development server binds 127.0.0.1:8443 with cert/key ssl_context; adhoc requires cryptography and remains development-only. @@ -10893,171 +11082,239 @@ Claims (full source mapping in the front matter): - verify-tls (REASONED; gunicorn, uvicorn): Public HTTPS must succeed without -k; certificate, chain, hostname and trust failures need investigation. - verify-auth (REASONED; django, fastapi): A real protected route denies no credentials with 401/403 and accepts valid file-supplied credentials with 2xx; logs or direct probing distinguish app from proxy enforcement. - verify-bind (REASONED; gunicorn, uvicorn): Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port. - - -Covers Flask, FastAPI/Uvicorn, Gunicorn, and Django. This guide's preferred production layout: bind the app server to `127.0.0.1` and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). For managed HTTP ingress, bind the listener required by your deployment; TLS termination depends on the platform and configuration described below ([paas.md](paas.md)). The app servers can also terminate TLS themselves, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). - -## 1. TLS per server - -Flask's built-in server (development only; it is not a production server, TLS or not): - -```python -app.run(host="127.0.0.1", port=8443, ssl_context=("cert.pem", "key.pem")) -# ssl_context="adhoc" generates a throwaway self-signed cert; requires the cryptography package -``` - -Gunicorn (Flask/Django/WSGI in production): - -```bash -gunicorn --bind 0.0.0.0:8443 \ - --certfile /etc/ssl/certs/server.crt \ - --keyfile /etc/ssl/private/server.key \ - app:app -``` - -Uvicorn (FastAPI/ASGI): - -```bash -uvicorn main:app --host 0.0.0.0 --port 8443 \ - --ssl-certfile /etc/ssl/certs/server.crt \ - --ssl-keyfile /etc/ssl/private/server.key -``` - -This guide's advice: Bind to `0.0.0.0` only when either (a) the process itself terminates TLS with authentication in place, or (b) a platform terminates TLS at its ingress and reaches your app over a private or container network. Otherwise, on a host you expose directly, keep `127.0.0.1` and terminate TLS in a reverse proxy. - -This guide's advice for managed platforms: do not add extra listeners; bind only the listener required by your deployment. The documented HTTP ingress mappings are: - -- Render web services must bind `0.0.0.0`. Render recommends the `PORT` environment variable (at the time of writing, default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound ([web services](https://render.com/docs/web-services)). -- Heroku assigns each web process its port through `PORT` ([runtime principles](https://devcenter.heroku.com/articles/runtime-principles)). Fir Private Spaces networking uses IPv6 by default; Heroku warns that IPv4-only assumptions do not bind properly and recommends an IPv6 or dual-stack listener, typically `::` ([generations](https://devcenter.heroku.com/articles/generations)). These pages do not establish Cedar's listener address. -- Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the configured `internal_port` to the app listener ([service binding](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42)). IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime ([wildcard qualification](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92)). -- Railway domains route to a detected or selected internal target port; the cited passage does not establish a required listener address, an injected `PORT` or the TLS termination location ([target ports](https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146)). - -Render's load balancer terminates inbound HTTPS and forwards HTTP to the web service ([web services](https://render.com/docs/web-services)). Heroku's HTTP routing page describes a load balancer offering SSL termination and routers forwarding HTTP to web dynos; it notes that both Private Spaces generations have different routers, so this is not a complete account of Private Spaces routing ([HTTP routing](https://devcenter.heroku.com/articles/http-routing)). Fly Proxy's `tls` handler terminates TLS and forwards plaintext ([TLS handler](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176)); without handlers it passes TCP through, allowing the app to terminate TLS itself ([TCP passthrough](https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97)). This guide's advice: plain HTTP is acceptable only behind protected ingress that terminates TLS; application authentication is still required ([paas.md](paas.md)). - -## 2. Django settings for HTTPS - -```python -SECURE_SSL_REDIRECT = True -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # only behind a proxy that STRIPS the client's value and sets its own -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True -SECURE_HSTS_SECONDS = 3600 # start small; raise to 31536000 (1 year) after HTTPS is confirmed working -SECURE_HSTS_INCLUDE_SUBDOMAINS = False # True ONLY after every subdomain also serves valid HTTPS; the commitment is irreversible for SECURE_HSTS_SECONDS -``` - -`SECURE_PROXY_SSL_HEADER` must be set only when a proxy you control, or a managed platform that reliably guarantees this, strips any client-supplied `X-Forwarded-Proto` from every request and sets it itself from the real connection scheme; otherwise a client can spoof it and make Django treat plain HTTP as HTTPS. Leave it unset if you are not behind such a proxy, and confirm the app is reachable only through that proxy, not directly. Run `python manage.py check --deploy` and fix what it reports. - -## 3. Authentication - -Follow [authentication.md](authentication.md). Framework specifics: - -- Keep Django's built-in password hashing; do not replace it with custom code (this guide's advice). -- If your Flask or FastAPI application implements its own password accounts, provide its account storage and hash passwords with `argon2-cffi` or `bcrypt` (this guide's advice; follow the algorithm and legacy-bcrypt constraints in [authentication.md](authentication.md)): - -```python -from argon2 import PasswordHasher -ph = PasswordHasher() -hash_ = ph.hash(password) -ph.verify(hash_, password) # raises on mismatch -``` - -The [argon2-cffi 25.1.0 example](https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28) documents these `PasswordHasher`, `hash` and `verify` calls and a `VerifyMismatchError` on mismatch. The [pinned bcrypt example](https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76) documents `hashpw(password, gensalt())` and `checkpw(password, hashed)`; these API examples do not establish framework user-store capabilities. - -- Generate tokens and secrets with the standard library, and load them from the environment (this guide's advice): - -```python -import secrets -token = secrets.token_urlsafe(32) -``` - -In [CPython v3.14.8](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103), `token_urlsafe(nbytes=None)` returns URL-safe Base64 text from `nbytes` random bytes; the argument counts input bytes, not output characters. The explicit `32` above is this guide's example choice, not a size suitable for every secret. If the argument is omitted or `None`, it uses `DEFAULT_ENTROPY`, [32 bytes at this pin](https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45); the [documented default can change](https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130), including in maintenance releases. - -- FastAPI's `fastapi.security` classes (`HTTPBearer`, `APIKeyHeader`, `OAuth2AuthorizationCodeBearer`, and so on) extract the credential from the request and declare the OpenAPI security scheme; what they check varies by helper (an API-key helper checks only that a value is present; the HTTP and OAuth2 bearer helpers may also check the scheme), but none authenticate the credential (no signature, issuer, audience, or expiry check), and `OpenIdConnect` is documented as a stub that does not implement the scheme or use the discovery URL. Use them to extract the token, then validate it (signature, issuer, audience, expiry) with an OIDC library such as Authlib, and authorize per [oidc-integration.md](oidc-integration.md). -- Rate-limit login routes (this guide's advice; for example with a proxy-level limit or a library such as slowapi for Starlette/FastAPI ASGI apps' HTTP endpoints; endpoints using slowapi's `limit` decorator must take an explicit `request` argument, and slowapi does not support WebSocket endpoints). -- MFA: add TOTP with [pyotp](https://github.com/pyauth/pyotp) plus the [qrcode](https://pypi.org/project/qrcode/) package for enrolment QR codes; [django-otp](https://pypi.org/project/django-otp/) integrates this into Django. Requirements and options in [mfa.md](mfa.md). - -## 4. Client-side TLS discipline - -Never ship `verify=False` (requests/httpx) or `ssl._create_unverified_context` (this guide's advice). For an internal CA, point the client at it instead (this guide's advice): - -```bash -export REQUESTS_CA_BUNDLE=/path/ca.crt # requests -export SSL_CERT_FILE=/path/ca.crt # HTTPX with verify=True and trust_env=True; OpenSSL default CA file for ssl when its default verify paths apply (see caveat below) -``` - -HTTPX 0.28.1's SSL guide says `SSL_CERT_FILE` and `SSL_CERT_DIR` are not automatically used and shows an explicit context; its code path contradicts that: its sync and async transports pass `verify` and `trust_env` to `create_ssl_context`, which reads `SSL_CERT_FILE` when both are true. Its environment guide says environment variables are used by default but does not list `SSL_CERT_FILE`. This guide's advice: use the export above for that HTTPX code path; pass an explicit CA context for other configurations. With no CA specified and verification enabled, CPython's `ssl.create_default_context()` tries to load default CAs (CPython notes this may fail silently). `ssl.get_default_verify_paths()` reports OpenSSL's environment key for a CA file; OpenSSL names `SSL_CERT_FILE` as an override for its default CA file. CPython's cited files do not name `SSL_CERT_FILE`, so whether a given `ssl` configuration uses that override remains unverified. - -## 5. Do not quick-share files with `http.server` - -`python -m http.server` is a common quick-share suggestion, and it is the wrong one on any reachable host. By default it binds every interface (as of Python 3.14.4), and it serves the current directory: source, `.env`, private keys, and database dumps are all downloadable by anyone who can reach the port. It also follows symbolic links, so a link in that directory hands out files from outside it, and the Python docs mark the module as not suitable for production. If you have no alternative, bind loopback and serve a directory that holds only what you mean to share: - -```bash -cd /path/to/a/directory/with/nothing/private || exit -python -m http.server -b 127.0.0.1 8000 -``` - -Reach it through an SSH tunnel, including one that runs over your tailnet, never a public bind. - -## 6. Verify (REASONED: TLS, authentication and listener expectations follow the Sources below and linked deployment guidance; no deployment run or outcome is recorded in this guide. This metadata-only review has no deployed Python application, proxy or managed ingress to test.) - -```bash -curl -q -sSI --noproxy '*' https://example.com/ # public HTTPS endpoint: must succeed without -k; if not, investigate the cert, chain, hostname, or your client trust store, never -k -# An unauthenticated request to a real protected path must be REJECTED and a valid credential ACCEPTED. --noproxy -# disables YOUR client proxy, not a reverse proxy in front of the app, so if the app sits behind an auth proxy this -# pair cannot tell whether the app or the proxy enforced it: confirm the layer by probing the app directly or in its logs. -curl -q -g -sS --noproxy '*' -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # no credentials: expect 401/403 -curl -q -g -sS --noproxy '*' -H @cred.txt -o /dev/null -w '%{http_code}\n' 'https://example.com/REPLACE_WITH_PROTECTED_PATH' # valid credential from a file (cred.txt holds a full header line, e.g. "Authorization: Bearer ..."): expect 2xx -ss -tlnp # every listener: loopback for a same-host reverse proxy, or the platform's required address and port from section 1 on managed ingress -``` - -## Sources (checked September 2026) -- Python `http.server` (binds all interfaces by default, serves the current directory, follows symlinks, not for production) (Python 3.14.4): https://docs.python.org/3/library/http.server.html -- Gunicorn documentation (settings reference: bind, certfile, keyfile, ca_certs): https://gunicorn.org/reference/settings/ -- Uvicorn settings reference: https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md -- Django deployment checklist: https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/ -- argon2-cffi: https://argon2-cffi.readthedocs.io/ -- argon2-cffi PasswordHasher, hash and verify mismatch example (25.1.0, checked October 2026): https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28 -- Python bcrypt hashpw, gensalt and checkpw example (commit 2b4ba9ac84df972e8e81311d09559af8dc82ef33, checked October 2026): https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76 -- Werkzeug serving (`ssl_context="adhoc"` requires cryptography): https://werkzeug.palletsprojects.com/en/stable/serving/ -- FastAPI security reference: https://fastapi.tiangolo.com/reference/security/ ; `OpenIdConnect` source (stub warning): https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py -- `http.server` binds with `bind=None` and `AI_PASSIVE`, the wildcard address (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334 -- `python -m http.server`'s `-b/--bind` has no default value, and its help says "default: all interfaces" (pinned tag v3.14.4): https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373 -- Python 3.14 secrets.token_urlsafe (series documentation, checked October 2026): https://docs.python.org/3.14/library/secrets.html -- CPython token_urlsafe input bytes and Base64 output (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103 -- CPython secrets default entropy and token_bytes fallback (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45 -- CPython secrets default may change in maintenance releases (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130 -- Python 3.14 os.getenv environment loading (series documentation, checked October 2026): https://docs.python.org/3.14/library/os.html#os.getenv -- PyOTP one-time passwords and MFA (v2.9.0, checked October 2026): https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5 -- qrcode enrolment image generation (v8.2, checked October 2026): https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44 -- django-otp Django authentication integration (1.6.1, checked October 2026): https://pypi.org/project/django-otp/1.6.1/ -- Requests REQUESTS_CA_BUNDLE trust roots (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239 -- slowapi Starlette/FastAPI scope (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3 -- slowapi limit decorator, endpoint request argument and WebSocket limitation (v0.1.10, checked October 2026): https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50 -- HTTPX environment-variable default and trust_env (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7 -- HTTPX environment guide variable list, which omits SSL_CERT_FILE (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53 -- HTTPX SSL guide, verify=False example (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17 -- HTTPX SSL guide, conflicting SSL_CERT_FILE statement (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86 -- HTTPX SSL context selection with verify=True and trust_env=True (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40 -- HTTPX sync transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154 -- HTTPX async transport passes verify and trust_env to create_ssl_context (0.28.1, checked October 2026): https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298 -- CPython ssl get_default_verify_paths (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405 -- CPython ssl load_default_certs (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548 -- CPython ssl create_default_context conditional default-CA load (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736 -- CPython ssl _create_unverified_context (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754 -- CPython ssl documentation for default verify paths and OpenSSL environment keys (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433 -- CPython ssl documentation for default CAs and silent failure (v3.14.8, checked October 2026): https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649 -- OpenSSL default CA file environment override (openssl-4.0.3, checked October 2026): https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49 -- Requests verify=False disables certificate verification (v2.32.3, checked October 2026): https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243 -- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services -- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing -- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles -- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations -- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 -- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 -- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 -- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 -- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "325223859acb920caba7e11658d3c3b87a9f7703faeab9e05b7641db8696fd48", + "components": { + "django": { + "name": "Django documentation", + "basis": "6.1", + "sources": { + "s7289e1c111ae": "https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/" + } + }, + "python": { + "name": "Python http.server documentation", + "basis": "3.14.4", + "sources": { + "s8fae94406709": "https://docs.python.org/3/library/http.server.html" + } + }, + "python-pin": { + "name": "Python http.server source", + "basis": "v3.14.4", + "sources": { + "s0038729960a0": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1322-L1334", + "sa3111578d65d": "https://github.com/python/cpython/blob/v3.14.4/Lib/http/server.py#L1371-L1373" + } + }, + "stdlib-docs": { + "name": "Python standard library documentation", + "basis": "3.14", + "sources": { + "s971d128efc65": "https://docs.python.org/3.14/library/secrets.html", + "sdad376a711c3": "https://docs.python.org/3.14/library/os.html#os.getenv" + } + }, + "cpython-ssl": { + "name": "CPython ssl source and documentation", + "basis": "v3.14.8", + "sources": { + "sbac049853cda": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L390-L405", + "s6dcc06feaaaf": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L542-L548", + "s4d7f143c9456": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L730-L736", + "s4a1cd173d596": "https://github.com/python/cpython/blob/v3.14.8/Lib/ssl.py#L744-L754", + "s79d6191e12eb": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L421-L433", + "s82c72b9a971a": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/ssl.rst#L1642-L1649" + } + }, + "cpython-secrets": { + "name": "CPython secrets documentation and source", + "basis": "v3.14.8", + "sources": { + "s31e1a792a6b1": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L91-L103", + "s7cc14a5ebfa0": "https://github.com/python/cpython/blob/v3.14.8/Lib/secrets.py#L31-L45", + "sdef0e1600064": "https://github.com/python/cpython/blob/v3.14.8/Doc/library/secrets.rst#L125-L130" + } + }, + "gunicorn": { + "name": "Gunicorn documentation", + "basis": "unknown", + "sources": { + "s72daf362300c": "https://gunicorn.org/reference/settings/" + } + }, + "uvicorn": { + "name": "Uvicorn source", + "basis": "5ac6265a01ff6dcadb0e4250152c3deaf8a168a9", + "sources": { + "s02429a4a09d9": "https://github.com/Kludex/uvicorn/blob/5ac6265a01ff6dcadb0e4250152c3deaf8a168a9/docs/settings.md" + } + }, + "fastapi": { + "name": "FastAPI documentation", + "basis": "unknown", + "sources": { + "s077aff608a5c": "https://fastapi.tiangolo.com/reference/security/" + } + }, + "oidc-stub": { + "name": "FastAPI OpenIdConnect source", + "basis": "31bbb380748ccead62fc0f42dbf4273f11dadccf", + "sources": { + "s876e3d51e7ac": "https://github.com/fastapi/fastapi/blob/31bbb380748ccead62fc0f42dbf4273f11dadccf/fastapi/security/open_id_connect_url.py" + } + }, + "bcrypt-api": { + "name": "Python bcrypt API example", + "basis": "2b4ba9ac84df972e8e81311d09559af8dc82ef33", + "sources": { + "s340a2923b6da": "https://github.com/pyca/bcrypt/blob/2b4ba9ac84df972e8e81311d09559af8dc82ef33/README.rst#L62-L76" + } + }, + "werkzeug": { + "name": "Werkzeug documentation", + "basis": "unknown", + "sources": { + "sd5066c63a3ca": "https://werkzeug.palletsprojects.com/en/stable/serving/" + } + }, + "argon": { + "name": "argon2-cffi documentation", + "basis": "unknown", + "sources": { + "sff2b9a487e67": "https://argon2-cffi.readthedocs.io/" + } + }, + "pyotp": { + "name": "PyOTP", + "basis": "v2.9.0", + "sources": { + "sc118b8ed3bdd": "https://github.com/pyauth/pyotp/blob/v2.9.0/README.rst#L4-L5" + } + }, + "qrcode": { + "name": "qrcode", + "basis": "v8.2", + "sources": { + "s8247ddfed3d3": "https://github.com/lincolnloop/python-qrcode/blob/v8.2/README.rst#L41-L44" + } + }, + "django-otp": { + "name": "django-otp", + "basis": "1.6.1", + "sources": { + "sa5c0f7097219": "https://pypi.org/project/django-otp/1.6.1/" + } + }, + "requests": { + "name": "Requests", + "basis": "v2.32.3", + "sources": { + "s21c6be6f6df4": "https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L238-L239", + "s17c5a0a7ffb2": "https://github.com/psf/requests/blob/v2.32.3/docs/user/advanced.rst#L241-L243" + } + }, + "slowapi": { + "name": "slowapi source", + "basis": "v0.1.10", + "sources": { + "sf150aa58aa31": "https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L1-L3", + "s4ca3e1e80817": "https://github.com/laurentS/slowapi/blob/v0.1.10/README.md#L24-L50" + } + }, + "httpx": { + "name": "HTTPX source and documentation", + "basis": "0.28.1", + "sources": { + "s52608d6c5e47": "https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L3-L7", + "sd33b79237122": "https://github.com/encode/httpx/blob/0.28.1/docs/environment_variables.md#L9-L53", + "s928d85a8d13e": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L12-L17", + "s3734fbccdf40": "https://github.com/encode/httpx/blob/0.28.1/docs/advanced/ssl.md#L72-L86", + "s30dd14bf172e": "https://github.com/encode/httpx/blob/0.28.1/httpx/_config.py#L23-L40", + "s4a1f3b43f184": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L133-L154", + "sc5a61a9b2f64": "https://github.com/encode/httpx/blob/0.28.1/httpx/_transports/default.py#L278-L298" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "openssl-4.0.3", + "sources": { + "s592eb65718de": "https://github.com/openssl/openssl/blob/openssl-4.0.3/doc/man3/SSL_CTX_load_verify_locations.pod#L39-L49" + } + }, + "argon-api": { + "name": "argon2-cffi API example", + "basis": "25.1.0", + "sources": { + "s193f86db0a79": "https://github.com/hynek/argon2-cffi/blob/25.1.0/README.md#L11-L28" + } + }, + "render": { + "name": "Render documentation", + "basis": "unknown", + "sources": { + "s1820ac471b6c": "https://render.com/docs/web-services" + } + }, + "heroku": { + "name": "Heroku documentation", + "basis": "unknown", + "sources": { + "s370d4beec29a": "https://devcenter.heroku.com/articles/runtime-principles", + "s211f5fe76042": "https://devcenter.heroku.com/articles/generations", + "s0c5bf300cef4": "https://devcenter.heroku.com/articles/http-routing" + } + }, + "fly-pinned": { + "name": "Fly.io pinned documentation", + "basis": "2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9", + "sources": { + "s62fec91543a1": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42", + "s61c1e50d2c11": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92", + "s254c8c8a5edd": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97", + "s32999aa4c58c": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176" + } + }, + "railway": { + "name": "Railway documentation", + "basis": "72354493198302677fb6be6c692dd75c64cae2c6", + "sources": { + "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146" + } + } + }, + "claims": { + "bind": {"text": "This guide advises loopback for same-host TLS proxies, no extra managed listeners, and wildcard only for authenticated direct TLS or private TLS ingress; app auth remains required. Rules vary (Fir Private Spaces: IPv6/dual-stack recommended; Fly without handlers: TCP passthrough); Railway address/injected port/TLS location and Cedar address remain uncited.", "components": ["gunicorn", "uvicorn", "render", "heroku", "fly-pinned", "railway"], "sources": ["gunicorn:s72daf362300c", "uvicorn:s02429a4a09d9", "render:s1820ac471b6c", "heroku:s370d4beec29a", "heroku:s211f5fe76042", "heroku:s0c5bf300cef4", "fly-pinned:s62fec91543a1", "fly-pinned:s61c1e50d2c11", "fly-pinned:s254c8c8a5edd", "fly-pinned:s32999aa4c58c", "railway:sac1383056b65"], "status": "REASONED"}, + "flask-tls": {"text": "Flask development server binds 127.0.0.1:8443 with cert/key ssl_context; adhoc requires cryptography and remains development-only.", "components": ["werkzeug"], "sources": ["werkzeug:sd5066c63a3ca"], "status": "REASONED"}, + "gunicorn-tls": {"text": "Gunicorn binds 0.0.0.0:8443 with --certfile and --keyfile for direct TLS.", "components": ["gunicorn"], "sources": ["gunicorn:s72daf362300c"], "status": "REASONED"}, + "uvicorn-tls": {"text": "Uvicorn binds 0.0.0.0:8443 with --ssl-certfile and --ssl-keyfile for direct TLS.", "components": ["uvicorn"], "sources": ["uvicorn:s02429a4a09d9"], "status": "REASONED"}, + "django-redirect": {"text": "Set SECURE_SSL_REDIRECT=True.", "components": ["django"], "sources": ["django:s7289e1c111ae"], "status": "REASONED"}, + "django-proxy": {"text": "Set SECURE_PROXY_SSL_HEADER only behind a trusted proxy that strips and replaces X-Forwarded-Proto; prevent direct bypass.", "components": ["django"], "sources": ["django:s7289e1c111ae"], "status": "REASONED"}, + "django-cookies": {"text": "Set SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE true.", "components": ["django"], "sources": ["django:s7289e1c111ae"], "status": "REASONED"}, + "django-hsts": {"text": "Begin HSTS at 3600 seconds and include-subdomains false; raise duration and scope only after valid HTTPS is confirmed.", "components": ["django"], "sources": ["django:s7289e1c111ae"], "status": "REASONED"}, + "deploy-check": {"text": "Run manage.py check --deploy and fix its findings.", "components": ["django"], "sources": ["django:s7289e1c111ae"], "status": "REASONED"}, + "password": {"text": "Keep Django's built-in hashing; do not replace it with custom code (guide advice). For Flask/FastAPI apps implementing local password accounts, this guide advises account storage and argon2-cffi or bcrypt under authentication.md's constraints; argon2-cffi 25.1.0's PasswordHasher.verify raises VerifyMismatchError on mismatch, and these APIs do not establish framework user-store capabilities.", "components": ["django", "argon", "argon-api", "bcrypt-api"], "sources": ["django:s7289e1c111ae", "argon:sff2b9a487e67", "argon-api:s193f86db0a79", "bcrypt-api:s340a2923b6da"], "status": "REASONED"}, + "secrets": {"text": "Generate secrets.token_urlsafe(32) and load secrets from the environment (guide advice); 32 counts random input bytes and is this guide's example choice, not a universal size. CPython v3.14.8 uses DEFAULT_ENTROPY (32 bytes) when omitted or None, but that default may change, including in maintenance releases.", "components": ["django", "stdlib-docs", "cpython-secrets"], "sources": ["django:s7289e1c111ae", "stdlib-docs:s971d128efc65", "stdlib-docs:sdad376a711c3", "cpython-secrets:s31e1a792a6b1", "cpython-secrets:s7cc14a5ebfa0", "cpython-secrets:sdef0e1600064"], "status": "REASONED"}, + "credential-extraction": {"text": "FastAPI security helpers extract credentials and declare schemes; presence/scheme checks do not verify signature, issuer, audience or expiry.", "components": ["fastapi"], "sources": ["fastapi:s077aff608a5c"], "status": "REASONED"}, + "oidc": {"text": "OpenIdConnect is a stub and does not use its discovery URL; validate extracted tokens with an OIDC library and authorize separately.", "components": ["fastapi", "oidc-stub"], "sources": ["fastapi:s077aff608a5c", "oidc-stub:s876e3d51e7ac"], "status": "REASONED"}, + "login-limit": {"text": "Rate-limiting login routes and a proxy-level limit are this guide's advice. slowapi v0.1.10 documents limits for Starlette/FastAPI HTTP endpoints; endpoints using its limit decorator must take an explicit request argument, and WebSocket endpoints are not supported.", "components": ["fastapi", "slowapi"], "sources": ["fastapi:s077aff608a5c", "slowapi:sf150aa58aa31", "slowapi:s4ca3e1e80817"], "status": "REASONED"}, + "mfa": {"text": "Use pyotp and qrcode, django-otp for Django, or linked MFA guidance.", "components": ["django", "pyotp", "qrcode", "django-otp"], "sources": ["django:s7289e1c111ae", "pyotp:sc118b8ed3bdd", "qrcode:s8247ddfed3d3", "django-otp:sa5c0f7097219"], "status": "REASONED"}, + "client-validation": {"text": "Never ship requests/httpx verify=False or ssl._create_unverified_context; HTTPX documents verify=False and CPython defines _create_unverified_context. The prohibition is this guide's advice.", "components": ["python", "requests", "httpx", "cpython-ssl"], "sources": ["python:s8fae94406709", "requests:s17c5a0a7ffb2", "httpx:s928d85a8d13e", "cpython-ssl:s4a1cd173d596"], "status": "REASONED"}, + "client-ca": {"text": "REQUESTS_CA_BUNDLE selects a requests CA. HTTPX 0.28.1's transport passes verify and trust_env to create_ssl_context, which reads SSL_CERT_FILE when verify=True and trust_env=True; its SSL guide contradicts that source. Its environment guide says environment variables are used by default but does not list SSL_CERT_FILE. CPython's ssl module reports OpenSSL default verify paths; with no CA specified and verification enabled, ssl.create_default_context() tries to load default CAs, which may fail silently. OpenSSL names SSL_CERT_FILE as an environment override for its default CA file; CPython's cited files do not name that variable, so whether a given ssl configuration uses it remains unverified. The export and explicit-context choices are this guide's advice.", "components": ["python", "requests", "httpx", "cpython-ssl", "openssl"], "sources": ["python:s8fae94406709", "requests:s21c6be6f6df4", "httpx:s52608d6c5e47", "httpx:sd33b79237122", "httpx:s3734fbccdf40", "httpx:s30dd14bf172e", "httpx:s4a1f3b43f184", "httpx:sc5a61a9b2f64", "cpython-ssl:sbac049853cda", "cpython-ssl:s6dcc06feaaaf", "cpython-ssl:s4d7f143c9456", "cpython-ssl:s79d6191e12eb", "cpython-ssl:s82c72b9a971a", "openssl:s592eb65718de"], "status": "REASONED"}, + "http-server-bind": {"text": "As of Python 3.14.4, http.server binds every interface by default; explicitly bind 127.0.0.1:8000.", "components": ["python-pin"], "sources": ["python-pin:s0038729960a0", "python-pin:sa3111578d65d"], "status": "REASONED"}, + "http-server-files": {"text": "http.server serves the current directory and follows symlinks outside it; it is unsuitable for production. Share only a non-private directory through an SSH tunnel.", "components": ["python"], "sources": ["python:s8fae94406709"], "status": "REASONED"}, + "verify-tls": {"text": "Public HTTPS must succeed without -k; certificate, chain, hostname and trust failures need investigation.", "components": ["gunicorn", "uvicorn"], "sources": ["gunicorn:s72daf362300c", "uvicorn:s02429a4a09d9"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "A real protected route denies no credentials with 401/403 and accepts valid file-supplied credentials with 2xx; logs or direct probing distinguish app from proxy enforcement.", "components": ["django", "fastapi"], "sources": ["django:s7289e1c111ae", "fastapi:s077aff608a5c"], "status": "REASONED", "verify": [1]}, + "verify-bind": {"text": "Inspect every listener for loopback behind a same-host proxy or the required managed-ingress address and port.", "components": ["gunicorn", "uvicorn"], "sources": ["gunicorn:s72daf362300c", "uvicorn:s02429a4a09d9"], "status": "REASONED", "verify": [1]} + } +} +```` + ====================================================================== ==> docker.md @@ -13547,53 +13804,9 @@ Claims (full source mapping in the JSON block below): # Full-stack JS frameworks: SvelteKit, Nuxt, Vite - -**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): - -- adapter: SvelteKit adapter-node (@sveltejs/adapter-node@5.5.7) -- svelte: SvelteKit documentation (unknown) -- svelte-kit-pin: SvelteKit routing and hooks documentation (@sveltejs/kit@2.70.3) -- h3-pin: h3 request and Node adapter source at release commit (1.15.3) -- node-http: Node HTTP documentation (v22.22.1) -- nuxt-lock: Nuxt lockfile (v4.0.0) -- nuxt: Nuxt documentation (4.x) -- vite-pin: Vite bind source (v8.3.1) -- vite: Vite documentation (unknown) -- grep: GNU grep (unknown) -- nuxt-output: Nuxt output source (v4.0.0) -- nitro-output: Nitro output defaults (2.12.0) -- nitro-node: nitropack 2.12.0 source (65e514d873da1d1d9edd4d9576d48d90b13e0fac) -- nitro-output-docs: Nitro documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): +Written against SvelteKit adapter-node @sveltejs/adapter-node@5.5.7, SvelteKit routing and hooks documentation @sveltejs/kit@2.70.3, Nuxt lockfile v4.0.0, Nuxt documentation 4.x, Nuxt output source v4.0.0, Vite bind source v8.3.1, and h3 request and Node adapter source at release commit 1.15.3, plus 2 more recorded bases; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 claims, all reasoned from cited sources. -- adapter-bind (REASONED; adapter, svelte): adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000. -- adapter-origin (REASONED; svelte): Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy. -- adapter-ip (REASONED; svelte): ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling. -- csrf-check (REASONED; svelte): csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks. -- csrf-trust (REASONED; svelte): csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged. -- svelte-public (REASONED; svelte): PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out. -- svelte-private (REASONED; svelte): Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds. -- svelte-routes (REASONED; svelte, svelte-kit-pin): Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each. -- svelte-handle (REASONED; svelte, svelte-kit-pin, adapter): Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it. -- nuxt-handlers (REASONED; nuxt): Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler. -- nuxt-auth (REASONED; nuxt): Nuxt ships no built-in authentication; enforce access server-side, not with client redirects. -- nuxt-private (REASONED; nuxt): Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them. -- nuxt-public (REASONED; nuxt): runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration. -- vite-production (REASONED; vite): Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform. -- vite-bind (REASONED; vite-pin, vite): Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests. -- vite-hosts (REASONED; vite): server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names. -- verify-private (REASONED; nuxt, svelte): Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects. -- verify-secret (REASONED; grep, adapter, nuxt-output, nitro-output, vite-pin, vite, nitro-output-docs): Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root. -- verify-host (REASONED; vite, adapter, nitro-node, svelte-kit-pin, h3-pin, node-http, nuxt-lock): The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers. -- verify-proxy (REASONED; svelte, nuxt, adapter, nuxt-output, nitro-node): Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome. - +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. SvelteKit, Nuxt, and Vite-based apps built by AI assistants inherit the same traps as [nextjs.md](nextjs.md): a server bind that can default wide open (SvelteKit's Node adapter binds `0.0.0.0` as of adapter-node 5.5.7, though Vite's dev server defaults to `localhost` as of Vite 8.3.1), a proxy that has to be explicitly trusted before secure cookies and correct origins work, and a public-env prefix that ships anything given it straight to the browser. Each framework also has more than one server entry point (endpoints, server routes, load functions), and a check placed in only one of them leaves the others open, exactly as with Next.js layouts versus Server Actions. @@ -13758,6 +13971,250 @@ REASONED: Host, proxy cookie, redirect and trusted-proxy checks per the cited Sv - SvelteKit adapter-node `host = env('HOST', '0.0.0.0')` and `port = env('PORT', !path && '3000')`, with `path = env('SOCKET_PATH', false)`, where `env()` returns a variable's value whenever it is present, even empty, and the socket-activation branch that listens on the passed descriptor instead (pinned tag @sveltejs/adapter-node@5.5.7): https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12, https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50 and https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60 - GNU grep `-f -` ("When file is '-', read patterns from standard input"), `-l` ("print the name of each input file from which output would normally have been printed") and its exit status (0 if a line is selected, 1 if none, 2 on error): https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html , https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html and https://www.gnu.org/software/grep/manual/html_node/Exit-Status.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/frontend-frameworks.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): + +- adapter: SvelteKit adapter-node (@sveltejs/adapter-node@5.5.7) +- svelte-kit-pin: SvelteKit routing and hooks documentation (@sveltejs/kit@2.70.3) +- nuxt-lock: Nuxt lockfile (v4.0.0) +- nuxt: Nuxt documentation (4.x) +- nuxt-output: Nuxt output source (v4.0.0) +- vite-pin: Vite bind source (v8.3.1) +- svelte: SvelteKit documentation (unknown) +- vite: Vite documentation (unknown) +- h3-pin: h3 request and Node adapter source at release commit (1.15.3) +- node-http: Node HTTP documentation (v22.22.1) +- nitro-output: Nitro output defaults (2.12.0) +- nitro-node: nitropack 2.12.0 source (65e514d873da1d1d9edd4d9576d48d90b13e0fac) +- nitro-output-docs: Nitro documentation (rolling) (unknown) +- grep: GNU grep (unknown) + +Claims (full source mapping in the JSON block below): + +- adapter-bind (REASONED; adapter, svelte): adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000. +- adapter-origin (REASONED; svelte): Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy. +- adapter-ip (REASONED; svelte): ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling. +- csrf-check (REASONED; svelte): csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks. +- csrf-trust (REASONED; svelte): csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged. +- svelte-public (REASONED; svelte): PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out. +- svelte-private (REASONED; svelte): Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds. +- svelte-routes (REASONED; svelte, svelte-kit-pin): Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each. +- svelte-handle (REASONED; svelte, svelte-kit-pin, adapter): Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it. +- nuxt-handlers (REASONED; nuxt): Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler. +- nuxt-auth (REASONED; nuxt): Nuxt ships no built-in authentication; enforce access server-side, not with client redirects. +- nuxt-private (REASONED; nuxt): Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them. +- nuxt-public (REASONED; nuxt): runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration. +- vite-production (REASONED; vite): Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform. +- vite-bind (REASONED; vite-pin, vite): Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests. +- vite-hosts (REASONED; vite): server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names. +- verify-private (REASONED; nuxt, svelte): Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects. +- verify-secret (REASONED; grep, adapter, nuxt-output, nitro-output, vite-pin, vite, nitro-output-docs): Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root. +- verify-host (REASONED; vite, adapter, nitro-node, svelte-kit-pin, h3-pin, node-http, nuxt-lock): The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers. +- verify-proxy (REASONED; svelte, nuxt, adapter, nuxt-output, nitro-node): Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "c967988728d40cccfa0b3e543320cd6e6d4f7568099a9805fb91a941243d4c1d", + "components": { + "adapter": { + "name": "SvelteKit adapter-node", + "basis": "@sveltejs/adapter-node@5.5.7", + "sources": { + "s011f4e44c91a": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/env.js#L46-L50", + "s93f13e6b8d31": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L10-L12", + "sd23cac9a730f": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/index.js#L57-L60", + "s200d6e800c74": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L24-L38", + "sf17c5646623c": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/index.js#L118", + "s1ffeda9bbb37": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L69", + "s8d4fa8e7426b": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L243", + "sfd51397a510e": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L17-L24", + "s6152d66c6b19": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L95-L105", + "s7b3a11637166": "https://github.com/sveltejs/kit/blob/%40sveltejs/adapter-node%405.5.7/packages/adapter-node/src/handler.js#L209-L239" + } + }, + "svelte-kit-pin": { + "name": "SvelteKit routing and hooks documentation", + "basis": "@sveltejs/kit@2.70.3", + "sources": { + "sca07fbe19546": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/10-routing.md#L291-L332", + "sa9bbf285a91c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/20-load.md#L702-L714", + "s380cc33507e0": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/30-form-actions.md#L1-L5", + "sf5b873616d3c": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/30-advanced/20-hooks.md#L15-L36", + "s5ad7a525e0ea": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/documentation/docs/20-core-concepts/60-remote-functions.md#L1300-L1303", + "s6a78ad19d884": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L70-L78", + "s0969ea98a4f1": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/page/index.js#L178-L250", + "sffcf647d5328": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/data/index.js#L39-L97", + "sfb110948d2ca": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/client/client.js#L1217-L1235", + "s67fe32e7a18d": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L73-L106", + "sc4708064d4fd": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L111-L130", + "s95d5c7dd7a50": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L333-L343", + "sca363a35f3fc": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L456-L498", + "s89773d0c39e7": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/runtime/server/respond.js#L274-L306", + "sd1c295d2cf56": "https://github.com/sveltejs/kit/blob/%40sveltejs/kit%402.70.3/packages/kit/src/exports/node/index.js#L121-L160" + } + }, + "nuxt-lock": { + "name": "Nuxt lockfile", + "basis": "v4.0.0", + "sources": { + "se77b46c1e3ad": "https://github.com/nuxt/nuxt/blob/v4.0.0/pnpm-lock.yaml#L5204-L5205" + } + }, + "nuxt": { + "name": "Nuxt documentation", + "basis": "4.x", + "sources": { + "se2d1e4a2e433": "https://nuxt.com/docs/4.x/directory-structure/server", + "s32e447cb5ef4": "https://nuxt.com/docs/4.x/guide/going-further/runtime-config" + } + }, + "nuxt-output": { + "name": "Nuxt output source", + "basis": "v4.0.0", + "sources": { + "sa65d64f55ca1": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L163-L170", + "se1cb3e5b4959": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/src/core/nitro.ts#L653-L659", + "sd00a53729513": "https://github.com/nuxt/nuxt/blob/v4.0.0/packages/nuxt/package.json#L104" + } + }, + "vite-pin": { + "name": "Vite bind source", + "basis": "v8.3.1", + "sources": { + "s36532f775f7f": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/utils.ts#L1010-L1022", + "s8ba1c2b868b7": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/config.ts#L388-L396", + "s484948ac14b6": "https://github.com/vitejs/vite/blob/v8.3.1/packages/vite/src/node/build.ts#L395" + } + }, + "svelte": { + "name": "SvelteKit documentation", + "basis": "unknown", + "sources": { + "sc183fdb68165": "https://svelte.dev/docs/kit/$env-static-public", + "s59abf03bae6a": "https://svelte.dev/docs/kit/adapter-node", + "s329dda89ba78": "https://svelte.dev/docs/kit/configuration", + "se2fdc4fd5885": "https://svelte.dev/docs/kit/server-only-modules" + } + }, + "vite": { + "name": "Vite documentation", + "basis": "unknown", + "sources": { + "sae9dd90908fc": "https://vite.dev/config/server-options", + "s16927b810cff": "https://vite.dev/guide/cli", + "s7c70a12e43ed": "https://vite.dev/config/build-options", + "sba6dbfdd7590": "https://vite.dev/guide/ssr" + } + }, + "h3-pin": { + "name": "h3 request and Node adapter source at release commit", + "basis": "1.15.3", + "sources": { + "scfe5cba40964": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L275-L286", + "sdbe7e781611f": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/utils/request.ts#L333-L344", + "s218970f40ae0": "https://github.com/h3js/h3/blob/3d0f4d582d1ffa7390bf3a6d6748aaf9ce3ab9e6/src/adapters/node.ts#L55-L89" + } + }, + "node-http": { + "name": "Node HTTP documentation", + "basis": "v22.22.1", + "sources": { + "sdc846082a7e3": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/http.md#L3653-L3657" + } + }, + "nitro-output": { + "name": "Nitro output defaults", + "basis": "2.12.0", + "sources": { + "sbe9fd1f61fe0": "https://cdn.jsdelivr.net/npm/nitropack@2.12.0/dist/core/index.mjs" + } + }, + "nitro-node": { + "name": "nitropack 2.12.0 source", + "basis": "65e514d873da1d1d9edd4d9576d48d90b13e0fac", + "sources": { + "sd56c769d6dae": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/_resolve.ts#L85-L90", + "s32975278fc3f": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/preset.ts#L16-L29", + "sdfda9cd3ce7b": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/presets/node/runtime/node-server.ts#L1-L23", + "sdf38c24e1f80": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/runtime/internal/app.ts#L59-L128", + "s91ab58ddc148": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/src/core/config/loader.ts#L134-L141", + "s0c8ca22542d2": "https://github.com/nitrojs/nitro/blob/65e514d873da1d1d9edd4d9576d48d90b13e0fac/package.json#L131" + } + }, + "nitro-output-docs": { + "name": "Nitro documentation (rolling)", + "basis": "unknown", + "sources": { + "s47fff9b24fba": "https://nitro.build/config", + "s51e38e411bfa": "https://nitro.build/docs/assets" + } + }, + "grep": { + "name": "GNU grep", + "basis": "unknown", + "sources": { + "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", + "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", + "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" + } + } + }, + "claims": { + "adapter-bind": {"text": "adapter-node defaults 0.0.0.0:3000 only with SOCKET_PATH unset/empty and no systemd socket; set HOST=127.0.0.1 PORT=3000.", "components": ["adapter", "svelte"], "sources": ["adapter:s011f4e44c91a", "adapter:s93f13e6b8d31", "adapter:sd23cac9a730f", "svelte:s59abf03bae6a"], "status": "REASONED"}, + "adapter-origin": {"text": "Set ORIGIN to external HTTPS origin; trust PROTOCOL_HEADER/HOST_HEADER only behind a trusted proxy.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, + "adapter-ip": {"text": "ADDRESS_HEADER and XFF_DEPTH configure proxy-derived client IP and require trusted proxy handling.", "components": ["svelte"], "sources": ["svelte:s59abf03bae6a"], "status": "REASONED"}, + "csrf-check": {"text": "csrf.checkOrigin defaults true, deprecated; checks cross-origin POST/PUT/PATCH/DELETE form content types, not JSON/arbitrary content needing origin/session checks.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, + "csrf-trust": {"text": "csrf.trustedOrigins defaults empty; list specific form origins, with wildcard trust discouraged.", "components": ["svelte"], "sources": ["svelte:s329dda89ba78"], "status": "REASONED"}, + "svelte-public": {"text": "PUBLIC_ via env.publicPrefix exposes static public env values at build; keep secrets out.", "components": ["svelte"], "sources": ["svelte:sc183fdb68165", "svelte:s329dda89ba78"], "status": "REASONED"}, + "svelte-private": {"text": "Private env modules, .server.js and lib/server are server-only; client import chains, including dynamic imports, fail builds.", "components": ["svelte"], "sources": ["svelte:se2fdc4fd5885"], "status": "REASONED"}, + "svelte-routes": {"text": "Pinned load docs and the SvelteKit client and server paths show layout `load` does not run on every request and layout/page `load` run concurrently unless a child calls `await parent()`; routing docs say `+layout` has no effect on `+server.js`; form-actions docs and page dispatch show actions are exported from `+page.server.js` and run before page loads. Guide advice: do not rely on a layout guard to protect a sibling page load, form action or directly reached endpoint; check the session before protected work in each.", "components": ["svelte", "svelte-kit-pin"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sca07fbe19546", "svelte-kit-pin:sa9bbf285a91c", "svelte-kit-pin:s380cc33507e0", "svelte-kit-pin:sfb110948d2ca", "svelte-kit-pin:sffcf647d5328", "svelte-kit-pin:s0969ea98a4f1", "svelte-kit-pin:s6a78ad19d884"], "status": "REASONED"}, + "svelte-handle": {"text": "Pinned hooks docs describe `handle` for SvelteKit server requests, including prerendering; runtime source calls it after checks that can reject requests before the hook and lets it return before `resolve`. In adapter-node 5.5.7, client assets and already prerendered pages are served before SSR, so they do not reach `handle` through that adapter. For client-initiated remote function requests, runtime source takes the calling-page path and search from request headers and resolves `route` and `params` from that path; pinned remote-functions docs describe the same fields on `RequestEvent` inside remote functions, and both docs warn never to use these request-derived fields to authorize data access. Guide advice: use `handle` to block unauthorized SvelteKit requests that reach it; a redirect-only or `event.locals`-only hook leaves directly reached endpoints and form actions open unless they check the session; do not rely on calling-page fields alone, and authorize each specific resource where served because a session check alone does not authorize it.", "components": ["svelte", "svelte-kit-pin", "adapter"], "sources": ["svelte:s329dda89ba78", "svelte-kit-pin:sf5b873616d3c", "svelte-kit-pin:s5ad7a525e0ea", "svelte-kit-pin:s67fe32e7a18d", "svelte-kit-pin:sc4708064d4fd", "svelte-kit-pin:s95d5c7dd7a50", "svelte-kit-pin:sca363a35f3fc", "svelte-kit-pin:s89773d0c39e7", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b"], "status": "REASONED"}, + "nuxt-handlers": {"text": "Nuxt auto-registers server/api, server/routes and middleware; middleware runs first, but context.auth alone enforces nothing. Throw/end requests or check each handler.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, + "nuxt-auth": {"text": "Nuxt ships no built-in authentication; enforce access server-side, not with client redirects.", "components": ["nuxt"], "sources": ["nuxt:se2d1e4a2e433"], "status": "REASONED"}, + "nuxt-private": {"text": "Direct runtimeConfig keys are server-only with NUXT_ overrides; rendering or passing to useState can disclose them.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, + "nuxt-public": {"text": "runtimeConfig.public is client-exposed with NUXT_PUBLIC_ overrides, not proxy/cookie configuration.", "components": ["nuxt"], "sources": ["nuxt:s32e447cb5ef4"], "status": "REASONED"}, + "vite-production": {"text": "Vite dev/preview is development tooling; deploy dist through a production server, CDN or platform.", "components": ["vite"], "sources": ["vite:s16927b810cff"], "status": "REASONED"}, + "vite-bind": {"text": "Vite 8.3.1 server.host defaults localhost; true/0.0.0.0 exposes all addresses and should not remain beyond trusted-network tests.", "components": ["vite-pin", "vite"], "sources": ["vite-pin:s36532f775f7f", "vite:sae9dd90908fc"], "status": "REASONED"}, + "vite-hosts": {"text": "server.allowedHosts defaults empty with localhost, .localhost and IPs allowed; true disables protection and permits DNS rebinding. Prefer explicit names.", "components": ["vite"], "sources": ["vite:sae9dd90908fc"], "status": "REASONED"}, + "verify-private": {"text": "Guide expects anonymous /api/private 401; enforcement needs app/server handlers, not client redirects.", "components": ["nuxt", "svelte"], "sources": ["nuxt:se2d1e4a2e433", "svelte:s329dda89ba78"], "status": "REASONED", "verify": [1]}, + "verify-secret": {"text": "Scan browser-served output: adapter-node build/client and build/prerendered, Nuxt .output/public, Vite dist, scanned whole following symlinks; only a result with every match under dist/server/ is reported as server output, else FINDING with dist/server/ matches labelled. Adjust paths for custom layouts/SSR; reject absence. Prompt full literal secret; grep stdin: 0 finding, 1 no match, others errors. Clean cannot rule out transformed secrets or runtime responses; hidden input and set +x keep the secret from argv, history and tracing, not the account owner or root.", "components": ["grep", "adapter", "nuxt-output", "nitro-output", "vite-pin", "vite", "nitro-output-docs"], "sources": ["grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae", "adapter:s200d6e800c74", "adapter:sf17c5646623c", "adapter:s1ffeda9bbb37", "adapter:s8d4fa8e7426b", "nuxt-output:sa65d64f55ca1", "nuxt-output:se1cb3e5b4959", "nitro-output:sbe9fd1f61fe0", "nitro-output-docs:s47fff9b24fba", "nitro-output-docs:s51e38e411bfa", "vite-pin:s8ba1c2b868b7", "vite-pin:s484948ac14b6", "vite:s7c70a12e43ed", "vite:sba6dbfdd7590"], "status": "REASONED", "verify": [1]}, + "verify-host": {"text": "The deployed application must not trust a spoofed Host. The supplied Vite allowlist reference covers its dev server. Guide assessment: for this probe, exposed means the application responds to the spoofed Host, so curl prints its first response line. Fixed means a fronting proxy or server refuses the unknown Host before the application; apply the default-server refusal pattern in [nginx.md](nginx.md) to the probed HTTPS listener. With a refusal response, curl prints its status line; with nginx return 444 closing without a response, this silent curl pipeline prints nothing. Neither a status line nor silence alone proves which layer handled the request or whether the application builds URLs or redirects from Host. Correlate proxy and application logs with the probe and a working allowed-host control; inspect response headers and generated URLs separately to judge Host use. In the traced adapter-node 5.5.7 and SvelteKit Node request conversion, ORIGIN or configured/request headers form the request URL; these files do not apply a Host allowlist. In the traced nitropack 2.12.0 node-server and app construction, the h3 app is passed to Node HTTP or HTTPS without a Host allowlist. Nuxt v4.0.0 resolves h3 1.15.3; its traced request utilities read Host or optionally forwarded Host and construct a URL without an allowlist; the release commit exports toNodeListener, which creates an event and calls app.handler, matching the adapter Nitro imports. Node v22.22.1 documents a default missing-Host rejection for HTTP/1.1, not an allowed-host list. Guide assessment: these source paths do not establish a universal Host rejection outcome or how the hosting platform treats Host and forwarded headers.", "components": ["vite", "adapter", "nitro-node", "svelte-kit-pin", "h3-pin", "node-http", "nuxt-lock"], "sources": ["vite:sae9dd90908fc", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:s0c8ca22542d2", "svelte-kit-pin:sd1c295d2cf56", "h3-pin:scfe5cba40964", "h3-pin:sdbe7e781611f", "h3-pin:s218970f40ae0", "node-http:sdc846082a7e3", "nuxt-lock:se77b46c1e3ad"], "status": "REASONED", "verify": [1]}, + "verify-proxy": {"text": "Check Secure cookies and HTTPS redirects with SvelteKit ORIGIN; verify Nuxt for its deployment preset and hosting platform, not NUXT_PUBLIC_. Adapter-node 5.5.7 uses ORIGIN or configured/request headers for the request origin; with ORIGIN and PROTOCOL_HEADER unset, it defaults to https and uses the request Host header unless HOST_HEADER supplies one. Nuxt 4.0.0 pins nitropack 2.12.0, which falls back to its node-server preset for a non-static production build with no preset named or matched; that preset passes the h3 app through toNodeListener to a Node HTTP or HTTPS server. Guide assessment: Nuxt's own trusted-proxy and origin handling come from its deployment preset and hosting platform rather than one documented app-level variable; the cited lines do not establish how h3 1.x or the hosting platform treats Host and forwarded headers or a deployed cookie and redirect outcome.", "components": ["svelte", "nuxt", "adapter", "nuxt-output", "nitro-node"], "sources": ["svelte:s59abf03bae6a", "nuxt:s32e447cb5ef4", "adapter:sfd51397a510e", "adapter:s6152d66c6b19", "adapter:s7b3a11637166", "nuxt-output:sd00a53729513", "nitro-node:sd56c769d6dae", "nitro-node:s32975278fc3f", "nitro-node:sdfda9cd3ce7b", "nitro-node:sdf38c24e1f80", "nitro-node:s91ab58ddc148", "nitro-node:s0c8ca22542d2"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> container-hardening.md ====================================================================== @@ -14155,60 +14612,9 @@ Claims (full source mapping in the JSON block below): # Next.js: authentication that actually gates Route Handlers, Server Actions, and data - -**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): +Written against Next.js listener source v16.3.6, Node.js listener source v22.22.1, and Commander 12.1.0; 6 further sources carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 32 claims, all reasoned from cited sources. -- next: Next.js documentation (unknown) -- next-pin: Next.js listener source (v16.3.6) -- node: Node.js listener source (v22.22.1) -- commander: Commander (12.1.0) -- authjs: Auth.js (unknown) -- better: Better Auth (unknown) -- vercel: Vercel documentation (unknown) -- curl: curl (unknown) -- grep: GNU grep (unknown) - -Claims (full source mapping in the front matter): - -- entrypoints (REASONED; next): Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login. -- start-default (REASONED; next-pin, node, commander): v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined. -- private-tls (REASONED; next): Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform. -- dal (REASONED; next): Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields. -- layouts (REASONED; next): Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions. -- proxy (REASONED; next): Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes. -- role (REASONED; next): Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses. -- session-secret (REASONED; next): Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_. -- session-jwt (REASONED; next): jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions. -- session-cookie (REASONED; next): Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session. -- public-env (REASONED; next): NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files. -- action-key (REASONED; next): Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection. -- action-origin (REASONED; next): Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins. -- authjs-secret (REASONED; authjs): AUTH_SECRET is mandatory; npx auth secret writes .env.local. -- authjs-providers (REASONED; authjs): Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks. -- authjs-trust (REASONED; authjs): Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically. -- authjs-route (REASONED; authjs): auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data. -- authjs-proxy (REASONED; authjs): auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider. -- better-secret (REASONED; better): BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference. -- better-origins (REASONED; better): trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks. -- better-password (REASONED; better): Better Auth emailAndPassword defaults disabled; enable explicitly when used. -- better-next (REASONED; better): nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes. -- better-session (REASONED; better): Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing. -- better-mfa (REASONED; better): twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks. -- vercel-methods (REASONED; vercel): Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication. -- vercel-scope (REASONED; vercel): Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby. -- verify-bind (REASONED; next): ss inventories loopback 3000; it does not prove firewall/platform ingress isolation. -- verify-anonymous (REASONED; next): Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies. -- verify-authorized (REASONED; next): Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success. -- verify-cookie-input (REASONED; curl): Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes. -- verify-secret (REASONED; next, grep): Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape. -- verify-action (REASONED; next): Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found. - +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 Next.js app has many entry points: pages, layouts, Route Handlers (`app/**/route.ts`), and every exported Server Action, which the Next.js docs describe as reachable by a direct POST whether or not your UI calls it. A check that lives only in a layout or in `proxy.ts` (the file Next.js 16 renamed from `middleware.ts`) leaves open the entry points it does not cover (a layout guards no Route Handler or Server Action; a `proxy.ts` matcher guards only the paths it matches), and authorization still belongs next to the data. Next.js supplies cookies and sessions as primitives, not a login system; the login is yours or a library's. @@ -14450,6 +14856,211 @@ curl -q -sS --noproxy '*' -o dash-nocookie.txt -w 'http=%{http_code} loc=%{redi - Next.js v16.3.6 pins `commander` at `12.1.0` (pinned tag v16.3.6): https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246 - curl `--header @-` (header lines read from stdin): https://curl.se/docs/manpage.html#-H ; GNU grep `-f -` ("When file is '-', read patterns from standard input"), `-l` ("print the name of each input file from which output would normally have been printed") and its exit status (0 if a line is selected, 1 if none, 2 on error): https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html , https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html and https://www.gnu.org/software/grep/manual/html_node/Exit-Status.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/nextjs.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): + +- next-pin: Next.js listener source (v16.3.6) +- next: Next.js documentation (unknown) +- node: Node.js listener source (v22.22.1) +- commander: Commander (12.1.0) +- authjs: Auth.js (unknown) +- better: Better Auth (unknown) +- vercel: Vercel documentation (unknown) +- curl: curl (unknown) +- grep: GNU grep (unknown) + +Claims (full source mapping in the JSON block below): + +- entrypoints (REASONED; next): Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login. +- start-default (REASONED; next-pin, node, commander): v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined. +- private-tls (REASONED; next): Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform. +- dal (REASONED; next): Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields. +- layouts (REASONED; next): Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions. +- proxy (REASONED; next): Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes. +- role (REASONED; next): Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses. +- session-secret (REASONED; next): Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_. +- session-jwt (REASONED; next): jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions. +- session-cookie (REASONED; next): Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session. +- public-env (REASONED; next): NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files. +- action-key (REASONED; next): Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection. +- action-origin (REASONED; next): Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins. +- authjs-secret (REASONED; authjs): AUTH_SECRET is mandatory; npx auth secret writes .env.local. +- authjs-providers (REASONED; authjs): Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks. +- authjs-trust (REASONED; authjs): Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically. +- authjs-route (REASONED; authjs): auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data. +- authjs-proxy (REASONED; authjs): auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider. +- better-secret (REASONED; better): BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference. +- better-origins (REASONED; better): trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks. +- better-password (REASONED; better): Better Auth emailAndPassword defaults disabled; enable explicitly when used. +- better-next (REASONED; better): nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes. +- better-session (REASONED; better): Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing. +- better-mfa (REASONED; better): twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks. +- vercel-methods (REASONED; vercel): Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication. +- vercel-scope (REASONED; vercel): Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby. +- verify-bind (REASONED; next): ss inventories loopback 3000; it does not prove firewall/platform ingress isolation. +- verify-anonymous (REASONED; next): Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies. +- verify-authorized (REASONED; next): Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success. +- verify-cookie-input (REASONED; curl): Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes. +- verify-secret (REASONED; next, grep): Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape. +- verify-action (REASONED; next): Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "3bdad54327114894a6f45d5d5adb334b0b4fbc9bf784629b0385e6e2c4063461", + "components": { + "next-pin": { + "name": "Next.js listener source", + "basis": "v16.3.6", + "sources": { + "s11ffc231f0c3": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/package.json#L246", + "s3b0feef8ec0f": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/bin/next.ts#L436-L458", + "s0ef9150d51e4": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L44", + "sefdefa3425c6": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/cli/next-start.ts#L84-L87", + "s66bd2ed9c346": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L187-L196", + "s2b4479b3c1d2": "https://github.com/vercel/next.js/blob/v16.3.6/packages/next/src/server/lib/start-server.ts#L309" + } + }, + "next": { + "name": "Next.js documentation", + "basis": "unknown", + "sources": { + "s066f8a176a52": "https://nextjs.org/docs/app/api-reference/cli/next", + "scd5749a71a79": "https://nextjs.org/docs/app/api-reference/file-conventions/proxy", + "sc2a39ce10fe1": "https://nextjs.org/docs/app/api-reference/file-conventions/route", + "sfbc4364fb5cc": "https://nextjs.org/docs/app/api-reference/functions/cookies", + "sa45045bebccb": "https://nextjs.org/docs/app/guides/authentication", + "s23a087193901": "https://nextjs.org/docs/app/guides/data-security", + "s964651d4eed4": "https://nextjs.org/docs/app/guides/environment-variables", + "s353c30dff99f": "https://nextjs.org/docs/app/guides/self-hosting" + } + }, + "node": { + "name": "Node.js listener source", + "basis": "v22.22.1", + "sources": { + "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" + } + }, + "commander": { + "name": "Commander", + "basis": "12.1.0", + "sources": { + "s408eb603ae2e": "https://github.com/tj/commander.js/blob/v12.1.0/Readme.md#L203-L205" + } + }, + "authjs": { + "name": "Auth.js", + "basis": "unknown", + "sources": { + "sb4ac5f50ec66": "https://authjs.dev/getting-started/deployment", + "s42365460cb4c": "https://authjs.dev/getting-started/installation", + "s46bb80456d4f": "https://authjs.dev/getting-started/session-management/protecting" + } + }, + "better": { + "name": "Better Auth", + "basis": "unknown", + "sources": { + "s981751bcb554": "https://better-auth.com/docs/installation", + "s2ac2e0926f25": "https://better-auth.com/docs/integrations/next", + "sdd3d825cc3a4": "https://better-auth.com/docs/introduction", + "s8677a74455a1": "https://better-auth.com/docs/plugins/2fa", + "s4b3a514c43d9": "https://better-auth.com/docs/reference/options" + } + }, + "vercel": { + "name": "Vercel documentation", + "basis": "unknown", + "sources": { + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection" + } + }, + "curl": { + "name": "curl", + "basis": "unknown", + "sources": { + "s5345a985007a": "https://curl.se/docs/manpage.html#-H" + } + }, + "grep": { + "name": "GNU grep", + "basis": "unknown", + "sources": { + "sea43e82b0753": "https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html", + "s2c1251092e66": "https://www.gnu.org/software/grep/manual/html_node/General-Output-Control.html", + "sd00017f70dae": "https://www.gnu.org/software/grep/manual/html_node/Matching-Control.html" + } + } + }, + "claims": { + "entrypoints": {"text": "Pages, layouts, handlers and exported Actions are distinct entry points; Actions accept direct POST. Next.js 16 renamed middleware.ts to proxy.ts; session primitives are not login.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sc2a39ce10fe1", "next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "start-default": {"text": "v16.3.6 next start defaults 3000 and no hostname: Node binds :: when available, else 0.0.0.0 despite -H help. Pinned Commander leaves unspecified value options undefined.", "components": ["next-pin", "node", "commander"], "sources": ["next-pin:s3b0feef8ec0f", "next-pin:s0ef9150d51e4", "next-pin:sefdefa3425c6", "next-pin:s66bd2ed9c346", "next-pin:s2b4479b3c1d2", "node:sfb3e306c102d", "commander:s408eb603ae2e", "next-pin:s11ffc231f0c3"], "status": "REASONED"}, + "private-tls": {"text": "Build then next start -H 127.0.0.1 -p 3000 with TLS proxy; PORT cannot be set in .env. Hosted TLS comes from the platform.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED"}, + "dal": {"text": "Call DAL verifySession for every data request, handler and Action; authorize resource ownership and return only needed fields.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "layouts": {"text": "Layouts do not rerender on every navigation; returning null does not stop nested segments or Actions.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "proxy": {"text": "Proxy is an optimistic cookie-only redirect layer, not sole authorization; avoid database lookups and account for matcher changes.", "components": ["next"], "sources": ["next:scd5749a71a79", "next:sa45045bebccb"], "status": "REASONED"}, + "role": {"text": "Handler/action examples independently require admin; sensitive roles come from trusted user storage. Fragments need imports, data calls and real success responses.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED"}, + "session-secret": {"text": "Generate a 32-byte random SESSION_SECRET in server environment, never NEXT_PUBLIC_.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s964651d4eed4"], "status": "REASONED"}, + "session-jwt": {"text": "jose session signs HS256 with seven-day expiry; decrypt verifies only HS256 and returns payload/undefined. User ID/role only, no PII; sensitive work uses database-verified sessions.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED"}, + "session-cookie": {"text": "Set HttpOnly, Secure, SameSite=Lax, path / and seven-day expiry; only Server Functions/handlers set/delete cookies, with logout deleting session.", "components": ["next"], "sources": ["next:sfbc4364fb5cc", "next:sa45045bebccb"], "status": "REASONED"}, + "public-env": {"text": "NEXT_PUBLIC_ is inlined at build; next.config.js env is client-inlined regardless of prefix. Read secrets only in DAL and ignore .env files.", "components": ["next"], "sources": ["next:s964651d4eed4", "next:s23a087193901"], "status": "REASONED"}, + "action-key": {"text": "Self-hosted instances share NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build, base64 decoding to 16/24/32 bytes; closure encryption is not secret protection.", "components": ["next"], "sources": ["next:s353c30dff99f", "next:s23a087193901"], "status": "REASONED"}, + "action-origin": {"text": "Actions abort when Origin mismatches Host/X-Forwarded-Host; proxies can configure experimental.serverActions.allowedOrigins.", "components": ["next"], "sources": ["next:s23a087193901", "next:s353c30dff99f"], "status": "REASONED"}, + "authjs-secret": {"text": "AUTH_SECRET is mandatory; npx auth secret writes .env.local.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, + "authjs-providers": {"text": "Configure provider ID/SECRET and OIDC ISSUER variables, real providers and linked allowlist checks.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66", "authjs:s42365460cb4c"], "status": "REASONED"}, + "authjs-trust": {"text": "Set AUTH_TRUST_HOST=true behind a reverse proxy; Vercel sets trust automatically.", "components": ["authjs"], "sources": ["authjs:sb4ac5f50ec66"], "status": "REASONED"}, + "authjs-route": {"text": "auth() reads sessions; wrapped handlers only populate req.auth and must deny missing sessions and failed authorization before data.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, + "authjs-proxy": {"text": "auth as proxy with empty providers and no authorized callback permits all requests; add callback/wrapper and resource checks. Enforce MFA at the identity provider.", "components": ["authjs"], "sources": ["authjs:s46bb80456d4f"], "status": "REASONED"}, + "better-secret": {"text": "BETTER_AUTH_SECRET needs 32+ characters; production rejects the placeholder default. Set BETTER_AUTH_URL/baseURL explicitly, not request inference.", "components": ["better"], "sources": ["better:s981751bcb554", "better:s4b3a514c43d9"], "status": "REASONED"}, + "better-origins": {"text": "trustedOrigins scopes origin/CSRF and callbackURL checks, not app-route access; general checks skip GET/HEAD/OPTIONS with endpoint-specific GET callback checks.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, + "better-password": {"text": "Better Auth emailAndPassword defaults disabled; enable explicitly when used.", "components": ["better"], "sources": ["better:s4b3a514c43d9"], "status": "REASONED"}, + "better-next": {"text": "nextCookies enables Action session-cookie writes; toNextJsHandler exports auth GET/POST routes.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, + "better-session": {"text": "Reject missing getSession before authorization: handler 401, component redirect/throw, action redirect/throw/serializable error. Discarding results or checking cookie existence gates nothing.", "components": ["better"], "sources": ["better:s2ac2e0926f25"], "status": "REASONED"}, + "better-mfa": {"text": "twoFactor makes TOTP, emailed/SMS OTP and backups available, not enrolled/enforced; OTP needs sendOTP, client needs twoFactorClient and migration, operations need separate 2FA checks.", "components": ["better"], "sources": ["better:s8677a74455a1"], "status": "REASONED"}, + "vercel-methods": {"text": "Deployment Protection admits project-authorized Vercel users; Passport, Password Protection and Trusted IPs need Enterprise/paid add-ons, not app-user authentication.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "vercel-scope": {"text": "Standard Protection excludes production domains; enable All Deployments. Cited 9 September 2026 announcement makes Vercel Authentication production scope free on all plans including Hobby.", "components": ["vercel"], "sources": ["vercel:s7836e043434a", "vercel:se6089d5c4e96"], "status": "REASONED"}, + "verify-bind": {"text": "ss inventories loopback 3000; it does not prove firewall/platform ingress isolation.", "components": ["next"], "sources": ["next:s066f8a176a52", "next:s353c30dff99f"], "status": "REASONED", "verify": [1]}, + "verify-anonymous": {"text": "Anonymous admin/dashboard must contain no protected data: login 3xx, chosen 401/403, or streamed 200 redirect shell require reading saved bodies.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, + "verify-authorized": {"text": "Authorized dashboard/admin controls must return real content; non-admin must deny admin data, meaningful only after admin success.", "components": ["next"], "sources": ["next:sa45045bebccb"], "status": "REASONED", "verify": [1]}, + "verify-cookie-input": {"text": "Prompt cookie values without echo and send through curl stdin; hidden prompt and set +x keep them from argv, history and tracing, not the account owner or root; assumes clean shell and separate subshell pastes.", "components": ["curl"], "sources": ["curl:s5345a985007a"], "status": "REASONED", "verify": [1]}, + "verify-secret": {"text": "Require .next/static, prompt full literal secret and send to grep stdin: 0 leak, 1 no literal match, other codes inconclusive. Encoded/split secrets can escape.", "components": ["next", "grep"], "sources": ["next:s964651d4eed4", "grep:sea43e82b0753", "grep:s2c1251092e66", "grep:sd00017f70dae"], "status": "REASONED", "verify": [1]}, + "verify-action": {"text": "Fresh disposable record per Action attempt; authorized mutation changes store, anonymous/unauthorized replays deny without changes, not merely not-found.", "components": ["next"], "sources": ["next:sa45045bebccb", "next:s23a087193901"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> go.md ====================================================================== @@ -14930,102 +15541,9 @@ Claims (full source mapping in the JSON block below): # ASP.NET Core and Kestrel: 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-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): - -- docs: ASP.NET Core documentation (.NET 10) -- rate-min: ASP.NET Core rate-limiter minimum (.NET 7) -- net10: ASP.NET Core (.NET 10) -- source: ASP.NET Core source (v10.0.0) -- proxy-fix: ASP.NET Core proxy hardening (8.0.17) -- runtime: NET runtime source (v10.0.0) -- openssl: OpenSSL documentation (3.0) -- precedence: WebApplicationBuilder precedence (.NET 7) -- iis: IIS documentation (IIS 10) -- iis-rolling: IIS documentation (rolling) (unknown) -- dotnet-rolling: .NET documentation (rolling) (unknown) -- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) - -Claims (full source mapping in the front matter): +Written against ASP.NET Core documentation .NET 10, ASP.NET Core rate-limiter minimum .NET 7, ASP.NET Core .NET 10, ASP.NET Core source v10.0.0, ASP.NET Core proxy hardening 8.0.17, NET runtime source v10.0.0, WebApplicationBuilder precedence .NET 7, and OpenSSL documentation 3.0, plus 1 more recorded basis; 1 further source is pinned to a commit and 2 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 71 claims, all reasoned from cited sources. -- tls (REASONED; docs, net10): Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store. -- tls-protocols (REASONED; net10): Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3. -- hsts-age (REASONED; source): NET 10 HSTS defaults to 30 days; example selects 365 days outside Development. -- hsts-subdomains (REASONED; source): HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS. -- hsts-preload (REASONED; source): HSTS Preload defaults false. -- redirect (REASONED; docs): UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort. -- proxy-bind (REASONED; docs, net10): ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first. -- proxy-default (REASONED; net10, source): NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list. -- proxy-networks (REASONED; net10, source): NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty. -- proxy-hardening (REASONED; net10, proxy-fix): Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy. -- proxy-https (REASONED; docs): When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops. -- password (REASONED; docs): Identity uses PBKDF2 with default 100000 iterations; keep its password hasher. -- lockout (REASONED; net10): Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false. -- cookies (REASONED; docs, net10): Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options. -- samesite (REASONED; net10): NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks. -- remote-cookies (REASONED; net10): Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting. -- authorization (REASONED; docs): Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages. -- login-limit (REASONED; rate-min): NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing. -- oidc (REASONED; docs): Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json. -- mfa (REASONED; docs, cf-docs): Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation. -- client-validation (REASONED; docs): Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback. -- client-ca (REASONED; runtime, openssl, dotnet-rolling): On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots. -- developer-errors (REASONED; net10): Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment. -- error-handler (REASONED; net10): Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses. -- environment (REASONED; net10, precedence): Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs. -- database-diagnostics (REASONED; net10): Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes. -- body-limit (REASONED; net10, source): NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes. -- body-override (REASONED; net10): RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection. -- multipart (REASONED; source, net10): MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413. -- iis-body (REASONED; net10): In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting. -- keepalive (REASONED; net10, source): Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline. -- header-timeout (REASONED; net10, source): Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline. -- connections (REASONED; source): MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100. -- upgraded (REASONED; net10, source): Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null. -- header-count (REASONED; source): Kestrel MaxRequestHeaderCount defaults 100. -- header-size (REASONED; source): Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes. -- request-rate (REASONED; source): MinRequestBodyDataRate defaults 240 bytes/second with five-second grace. -- response-rate (REASONED; source): MinResponseDataRate defaults 240 bytes/second with five-second grace. -- rate-scope (REASONED; net10): HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger. -- h2-streams (REASONED; source): NET 10 HTTP/2 MaxStreamsPerConnection defaults 100. -- h2-table (REASONED; source): NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes. -- h2-frame (REASONED; source): NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes. -- h2-header (REASONED; source): NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192. -- h2-connection-window (REASONED; source): NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072. -- h2-stream-window (REASONED; source): NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304. -- h2-ping-delay (REASONED; source): NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings. -- h2-ping-timeout (REASONED; source): NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds. -- h2-scope (REASONED; net10, source): Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3. -- key-defaults (REASONED; net10): Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys. -- key-persistence (REASONED; net10): Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names. -- key-encryption (REASONED; net10): Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates. -- key-isolation (REASONED; net10): Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys. -- cookie-sharing (REASONED; net10): Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration. -- server-header (REASONED; source): NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access. -- iis-headers (REASONED; iis, iis-rolling): Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later. -- openapi (REASONED; net10): NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy. -- swagger (REASONED; net10): Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document. -- health (REASONED; net10): Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route. -- health-host (REASONED; net10): RequireHost checks a spoofable Host header and is not a network or authentication boundary. -- host-filter (REASONED; net10): AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate. -- verify-redirect (REASONED; docs): HTTP should return 307/308 with HTTPS Location. -- verify-tls (REASONED; docs, net10): HTTPS must validate without -k and show HSTS. -- verify-api (REASONED; net10): Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first. -- verify-bind (REASONED; docs, net10): Inspect every listener for 127.0.0.1 and ::1 behind the proxy. -- verify-errors (REASONED; net10): Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution. -- verify-body (REASONED; net10): Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection. -- verify-aux (REASONED; net10): Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization. -- verify-migrations (REASONED; net10): GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure. -- verify-headers (REASONED; source, iis, iis-rolling): Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported. -- verify-cookie (REASONED; net10): Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes. -- verify-antiforgery (REASONED; net10): Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery. - +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. Preferred production layout: bind Kestrel to loopback and terminate TLS in a reverse proxy ([caddy.md](caddy.md), [nginx.md](nginx.md)) or behind [cloudflare.md](cloudflare.md). Kestrel can also terminate TLS itself, shown below. Certificates: [free-certificates.md](free-certificates.md) or [self-signed.md](self-signed.md). @@ -15480,6 +15998,328 @@ Also retain an antiforgery token and its accompanying cookie across a restart, t - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, self-hosted public application authentication layer (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#L14-L16 - Cloudflare documentation `c896795b8e8bab68d05ae78d64c9cbcda1fdb104`, publicly routable origin protection requirement (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#L48-L48 + + +## 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/dotnet.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): + +- docs: ASP.NET Core documentation (.NET 10) +- rate-min: ASP.NET Core rate-limiter minimum (.NET 7) +- net10: ASP.NET Core (.NET 10) +- source: ASP.NET Core source (v10.0.0) +- proxy-fix: ASP.NET Core proxy hardening (8.0.17) +- runtime: NET runtime source (v10.0.0) +- precedence: WebApplicationBuilder precedence (.NET 7) +- dotnet-rolling: .NET documentation (rolling) (unknown) +- openssl: OpenSSL documentation (3.0) +- iis: IIS documentation (IIS 10) +- iis-rolling: IIS documentation (rolling) (unknown) +- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) + +Claims (full source mapping in the JSON block below): + +- tls (REASONED; docs, net10): Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store. +- tls-protocols (REASONED; net10): Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3. +- hsts-age (REASONED; source): NET 10 HSTS defaults to 30 days; example selects 365 days outside Development. +- hsts-subdomains (REASONED; source): HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS. +- hsts-preload (REASONED; source): HSTS Preload defaults false. +- redirect (REASONED; docs): UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort. +- proxy-bind (REASONED; docs, net10): ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first. +- proxy-default (REASONED; net10, source): NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list. +- proxy-networks (REASONED; net10, source): NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty. +- proxy-hardening (REASONED; net10, proxy-fix): Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy. +- proxy-https (REASONED; docs): When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops. +- password (REASONED; docs): Identity uses PBKDF2 with default 100000 iterations; keep its password hasher. +- lockout (REASONED; net10): Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false. +- cookies (REASONED; docs, net10): Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options. +- samesite (REASONED; net10): NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks. +- remote-cookies (REASONED; net10): Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting. +- authorization (REASONED; docs): Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages. +- login-limit (REASONED; rate-min): NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing. +- oidc (REASONED; docs): Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json. +- mfa (REASONED; docs, cf-docs): Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation. +- client-validation (REASONED; docs): Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback. +- client-ca (REASONED; runtime, openssl, dotnet-rolling): On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots. +- developer-errors (REASONED; net10): Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment. +- error-handler (REASONED; net10): Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses. +- environment (REASONED; net10, precedence): Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs. +- database-diagnostics (REASONED; net10): Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes. +- body-limit (REASONED; net10, source): NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes. +- body-override (REASONED; net10): RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection. +- multipart (REASONED; source, net10): MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413. +- iis-body (REASONED; net10): In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting. +- keepalive (REASONED; net10, source): Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline. +- header-timeout (REASONED; net10, source): Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline. +- connections (REASONED; source): MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100. +- upgraded (REASONED; net10, source): Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null. +- header-count (REASONED; source): Kestrel MaxRequestHeaderCount defaults 100. +- header-size (REASONED; source): Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes. +- request-rate (REASONED; source): MinRequestBodyDataRate defaults 240 bytes/second with five-second grace. +- response-rate (REASONED; source): MinResponseDataRate defaults 240 bytes/second with five-second grace. +- rate-scope (REASONED; net10): HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger. +- h2-streams (REASONED; source): NET 10 HTTP/2 MaxStreamsPerConnection defaults 100. +- h2-table (REASONED; source): NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes. +- h2-frame (REASONED; source): NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes. +- h2-header (REASONED; source): NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192. +- h2-connection-window (REASONED; source): NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072. +- h2-stream-window (REASONED; source): NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304. +- h2-ping-delay (REASONED; source): NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings. +- h2-ping-timeout (REASONED; source): NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds. +- h2-scope (REASONED; net10, source): Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3. +- key-defaults (REASONED; net10): Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys. +- key-persistence (REASONED; net10): Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names. +- key-encryption (REASONED; net10): Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates. +- key-isolation (REASONED; net10): Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys. +- cookie-sharing (REASONED; net10): Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration. +- server-header (REASONED; source): NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access. +- iis-headers (REASONED; iis, iis-rolling): Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later. +- openapi (REASONED; net10): NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy. +- swagger (REASONED; net10): Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document. +- health (REASONED; net10): Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route. +- health-host (REASONED; net10): RequireHost checks a spoofable Host header and is not a network or authentication boundary. +- host-filter (REASONED; net10): AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate. +- verify-redirect (REASONED; docs): HTTP should return 307/308 with HTTPS Location. +- verify-tls (REASONED; docs, net10): HTTPS must validate without -k and show HSTS. +- verify-api (REASONED; net10): Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first. +- verify-bind (REASONED; docs, net10): Inspect every listener for 127.0.0.1 and ::1 behind the proxy. +- verify-errors (REASONED; net10): Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution. +- verify-body (REASONED; net10): Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection. +- verify-aux (REASONED; net10): Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization. +- verify-migrations (REASONED; net10): GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure. +- verify-headers (REASONED; source, iis, iis-rolling): Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported. +- verify-cookie (REASONED; net10): Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes. +- verify-antiforgery (REASONED; net10): Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "10f455bd5e1e9cc54ea69a2d50883c9cba79de0ada5085b56891741f8232ee5d", + "components": { + "docs": { + "name": "ASP.NET Core documentation", + "basis": ".NET 10", + "sources": { + "s8c762260991c": "https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-10.0", + "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", + "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", + "s3790c49f70b9": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-10.0", + "s9d10c561bb48": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions?view=aspnetcore-10.0", + "sa3c22a38f4cb": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-10.0", + "sb6780977fa3f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy?view=aspnetcore-10.0", + "s592dc85bb71f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0", + "s6afc0255098f": "https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.dangerousacceptanyservercertificatevalidator?view=net-10.0", + "s2d5a930aac35": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-10.0" + } + }, + "rate-min": { + "name": "ASP.NET Core rate-limiter minimum", + "basis": ".NET 7", + "sources": { + "sd3fef8f45066": "https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit" + } + }, + "net10": { + "name": "ASP.NET Core", + "basis": ".NET 10", + "sources": { + "s74dcd6a72d63": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0", + "s5701d26de241": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0", + "s0d7c367ac06f": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration?view=aspnetcore-10.0", + "sc0654e58fc4c": "https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-10.0", + "s837df4b1657f": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints?view=aspnetcore-10.0", + "s2f7e6a8213f1": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-10.0", + "s61afb89034f8": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-10.0", + "s9d290374f4e8": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint?view=aspnetcore-10.0", + "s1ce47370d948": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?view=aspnetcore-10.0", + "s55f298bf0048": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.disablerequestsizelimitattribute?view=aspnetcore-10.0", + "s792d3e8f3e42": "https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-10.0", + "s529a3d7cce08": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0", + "s6682926c8da7": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-10.0", + "s66f1ab4555d6": "https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-10.0", + "sd635cefc0a9f": "https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate?view=aspnetcore-10.0", + "s9463626a3d0e": "https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-10.0", + "s1d58219cedec": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0", + "s1f9d4c5e975c": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0", + "sc41b77cc06c8": "https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0", + "se101626c29d6": "https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0" + } + }, + "source": { + "name": "ASP.NET Core source", + "basis": "v10.0.0", + "sources": { + "sb62cff70e3b8": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpsPolicy/src/HstsOptions.cs", + "sfc86e3f62243": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs", + "s903da00d32b5": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs", + "sc00d8d67e93b": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/Http2Limits.cs", + "s67f347ba7947": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Http/Http/src/Features/FormOptions.cs", + "s18c44d6c2fb7": "https://raw.githubusercontent.com/dotnet/aspnetcore/v10.0.0/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs" + } + }, + "proxy-fix": { + "name": "ASP.NET Core proxy hardening", + "basis": "8.0.17", + "sources": { + "s2864662bcd21": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/8/forwarded-headers-unknown-proxies?view=aspnetcore-10.0" + } + }, + "runtime": { + "name": "NET runtime source", + "basis": "v10.0.0", + "sources": { + "s29ee940c94b9": "https://raw.githubusercontent.com/dotnet/runtime/v10.0.0/src/native/libs/System.Security.Cryptography.Native/pal_x509_root.c" + } + }, + "precedence": { + "name": "WebApplicationBuilder precedence", + "basis": ".NET 7", + "sources": { + "s10d46bc97fef": "https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/7/environment-variable-precedence?view=aspnetcore-10.0" + } + }, + "dotnet-rolling": { + "name": ".NET documentation (rolling)", + "basis": "unknown", + "sources": { + "s640872a17a8a": "https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "3.0", + "sources": { + "scac6dada5f6a": "https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/" + } + }, + "iis": { + "name": "IIS documentation", + "basis": "IIS 10", + "sources": { + "sb2eac755b583": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/" + } + }, + "iis-rolling": { + "name": "IIS documentation (rolling)", + "basis": "unknown", + "sources": { + "s267eddd43502": "https://learn.microsoft.com/en-us/iis/configuration/system.webserver/httpprotocol/customheaders/" + } + }, + "cf-docs": { + "name": "Cloudflare 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", + "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", + "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", + "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" + } + } + }, + "claims": { + "tls": {"text": "Kestrel serves wildcard HTTP 80 and HTTPS 443 with PKCS12 or PEM Path/KeyPath; supply certificate passwords from environment or a secret store.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED"}, + "tls-protocols": {"text": "Default SslProtocols.None delegates to the OS; example explicitly permits only TLS 1.2 and 1.3.", "components": ["net10"], "sources": ["net10:s74dcd6a72d63"], "status": "REASONED"}, + "hsts-age": {"text": "NET 10 HSTS defaults to 30 days; example selects 365 days outside Development.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "hsts-subdomains": {"text": "HSTS IncludeSubDomains defaults false; example enables it only when all affected subdomains support HTTPS.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "hsts-preload": {"text": "HSTS Preload defaults false.", "components": ["source"], "sources": ["source:sb62cff70e3b8"], "status": "REASONED"}, + "redirect": {"text": "UseHttpsRedirection needs an HTTPS port via ASPNETCORE_HTTPS_PORT=443 or HttpsPort.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED"}, + "proxy-bind": {"text": "ListenLocalhost(5000), or localhost:5000 URLs, binds behind a same-host proxy; process forwarded headers first.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s5701d26de241"], "status": "REASONED"}, + "proxy-default": {"text": "NET 10 trusts ::1 and 127.0.0.0/8 by default; adding 127.0.0.1 does not narrow that list.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, + "proxy-networks": {"text": "NET 10 replaces obsolete KnownNetworks with KnownIPNetworks/System.Net.IPNetwork; narrow both lists together and never leave both empty.", "components": ["net10", "source"], "sources": ["net10:s5701d26de241", "source:sfc86e3f62243"], "status": "REASONED"}, + "proxy-hardening": {"text": "Unknown-proxy headers are ignored in 8.0.17 and 9.0.6 and NET 10, even without X-Forwarded-For processing; register the real proxy.", "components": ["net10", "proxy-fix"], "sources": ["net10:s5701d26de241", "proxy-fix:s2864662bcd21"], "status": "REASONED"}, + "proxy-https": {"text": "When the proxy handles redirects/HSTS, omit the app middleware; missing forwarded-header handling can cause redirect loops.", "components": ["docs"], "sources": ["docs:s8c762260991c", "docs:s5701d26de241"], "status": "REASONED"}, + "password": {"text": "Identity uses PBKDF2 with default 100000 iterations; keep its password hasher.", "components": ["docs"], "sources": ["docs:s3790c49f70b9", "docs:s9d10c561bb48"], "status": "REASONED"}, + "lockout": {"text": "Configure five failures, five-minute lockout and allowance for new users; PasswordSignInAsync must use lockoutOnFailure=true, while the template uses false.", "components": ["net10"], "sources": ["net10:s0d7c367ac06f"], "status": "REASONED"}, + "cookies": {"text": "Configure HttpOnly, SecurePolicy.Always, SameSite=Lax and eight-hour expiry; cookie-only auth uses the same options.", "components": ["docs", "net10"], "sources": ["docs:sa3c22a38f4cb", "docs:sb6780977fa3f", "net10:s0d7c367ac06f"], "status": "REASONED"}, + "samesite": {"text": "NET 10 auth cookie defaults Lax; Strict can suppress cross-site app cookies without universally breaking OAuth/OIDC callbacks.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, + "remote-cookies": {"text": "Correlation and OIDC nonce cookies separately default None; preserve secure cross-site settings and avoid global rewriting.", "components": ["net10"], "sources": ["net10:sc0654e58fc4c"], "status": "REASONED"}, + "authorization": {"text": "Run authentication before authorization and Map calls; use an authenticated fallback policy and explicitly AllowAnonymous on public pages.", "components": ["docs"], "sources": ["docs:sa3c22a38f4cb"], "status": "REASONED"}, + "login-limit": {"text": "NET 7+ built-in rate limiting: login policy permits 20 per 15 minutes, no queue, 429 rejection; endpoint policies need UseRateLimiter after routing.", "components": ["rate-min"], "sources": ["rate-min:sd3fef8f45066"], "status": "REASONED"}, + "oidc": {"text": "Configure OpenIdConnect code flow with cookie DefaultScheme and OIDC DefaultChallengeScheme; keep client secrets outside appsettings.json.", "components": ["docs"], "sources": ["docs:s592dc85bb71f"], "status": "REASONED"}, + "mfa": {"text": "Enforce MFA at the identity provider or fronting layer; this guide recommends Cloudflare Access independent MFA for a self-hosted public app only with effective MFA enforcement (policy over application over organization) and no origin bypass. The cited pages do not establish this deployment's enforcement or origin isolation.", "components": ["docs", "cf-docs"], "sources": ["docs:s592dc85bb71f", "docs:s2d5a930aac35", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "cf-docs:sfddb92e438ca", "cf-docs:s863803bd9a1f"], "status": "REASONED"}, + "client-validation": {"text": "Never accept every certificate through DangerousAcceptAnyServerCertificateValidator or an always-true callback.", "components": ["docs"], "sources": ["docs:s6afc0255098f"], "status": "REASONED"}, + "client-ca": {"text": "On Linux NET 10 install internal roots or use SSL_CERT_FILE for a PEM file and SSL_CERT_DIR for a certificate directory; preserve needed public roots.", "components": ["runtime", "openssl", "dotnet-rolling"], "sources": ["dotnet-rolling:s640872a17a8a", "runtime:s29ee940c94b9", "openssl:scac6dada5f6a"], "status": "REASONED"}, + "developer-errors": {"text": "Development automatically enables the Developer Exception Page; production must exclude it and check the effective environment.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s61afb89034f8"], "status": "REASONED"}, + "error-handler": {"text": "Install UseExceptionHandler early after forwarding; provide a generic /Error route supporting failed methods/anonymous requests and preserve intentional 413 responses.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED"}, + "environment": {"text": "Production is default absent overrides; NET 7+ WebApplicationBuilder prioritizes command-line/DOTNET_ over ASPNETCORE_, while older WebHost differs.", "components": ["net10", "precedence"], "sources": ["net10:s61afb89034f8", "precedence:s10d46bc97fef"], "status": "REASONED"}, + "database-diagnostics": {"text": "Register database developer exception filters before Build and migrations middleware after Build only in Development; migrations execute database changes.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1", "net10:s9d290374f4e8"], "status": "REASONED"}, + "body-limit": {"text": "NET 10 Kestrel total body default is 30000000 bytes, null unlimited; example lowers it to 1048576 bytes.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "body-override": {"text": "RequestSizeLimit overrides before body reading; DisableRequestSizeLimit removes the protection.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s55f298bf0048"], "status": "REASONED"}, + "multipart": {"text": "MultipartBodyLengthLimit defaults 134217728 bytes per parsed section; example uses 1048576. Framing consumes total allowance and parsing failure need not return 413.", "components": ["source", "net10"], "sources": ["source:s67f347ba7947", "net10:s792d3e8f3e42"], "status": "REASONED"}, + "iis-body": {"text": "In-process IISServerOptions body default is 30000000, separately from IIS maxAllowedContentLength default 30000000; out-of-process IIS disables Kestrel body limiting.", "components": ["net10"], "sources": ["net10:s1ce47370d948", "net10:s792d3e8f3e42", "net10:s529a3d7cce08"], "status": "REASONED"}, + "keepalive": {"text": "Kestrel KeepAliveTimeout defaults to 130 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "header-timeout": {"text": "Kestrel RequestHeadersTimeout defaults to 30 seconds; it is not a total execution deadline.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "connections": {"text": "MaxConcurrentConnections defaults null/unlimited; example caps ordinary connections at 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "upgraded": {"text": "Upgraded connections leave the ordinary count; MaxConcurrentUpgradedConnections separately defaults null.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:s903da00d32b5"], "status": "REASONED"}, + "header-count": {"text": "Kestrel MaxRequestHeaderCount defaults 100.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "header-size": {"text": "Kestrel MaxRequestHeadersTotalSize defaults 32768 bytes.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "request-rate": {"text": "MinRequestBodyDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "response-rate": {"text": "MinResponseDataRate defaults 240 bytes/second with five-second grace.", "components": ["source"], "sources": ["source:s903da00d32b5"], "status": "REASONED"}, + "rate-scope": {"text": "HTTP/2 per-request rate adjustments differ from HTTP/1.x; several timeout/rate checks are disabled under a debugger.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED"}, + "h2-streams": {"text": "NET 10 HTTP/2 MaxStreamsPerConnection defaults 100.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-table": {"text": "NET 10 HTTP/2 HeaderTableSize defaults 4096 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-frame": {"text": "NET 10 HTTP/2 MaxFrameSize defaults 16384 bytes.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-header": {"text": "NET 10 HTTP/2 MaxRequestHeaderFieldSize defaults 32768 bytes, not the older 8192.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-connection-window": {"text": "NET 10 HTTP/2 InitialConnectionWindowSize defaults 1048576 bytes, not the older 131072.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-stream-window": {"text": "NET 10 HTTP/2 InitialStreamWindowSize defaults 786432 bytes, not the older 98304.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-ping-delay": {"text": "NET 10 HTTP/2 KeepAlivePingDelay defaults TimeSpan.MaxValue, disabling pings.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-ping-timeout": {"text": "NET 10 HTTP/2 KeepAlivePingTimeout defaults 20 seconds.", "components": ["source"], "sources": ["source:sc00d8d67e93b"], "status": "REASONED"}, + "h2-scope": {"text": "Flow-control windows bound outstanding buffered data, not total bodies; Http2Limits do not configure HTTP/3.", "components": ["net10", "source"], "sources": ["net10:s1ce47370d948", "source:sc00d8d67e93b"], "status": "REASONED"}, + "key-defaults": {"text": "Data Protection key persistence depends on hosting: user profiles, IIS or Azure may persist; process-only fallback and disposable container layers can lose keys.", "components": ["net10"], "sources": ["net10:s66f1ab4555d6"], "status": "REASONED"}, + "key-persistence": {"text": "Persist keys durably with restricted permissions; replicas share repository, application name and decryption certificates, not merely identical directory names.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, + "key-encryption": {"text": "Choosing an explicit repository disables automatic at-rest encryption; configure ProtectKeysWithCertificate and retain needed keys/certificates.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:sd635cefc0a9f"], "status": "REASONED"}, + "key-isolation": {"text": "Separate repositories, permissions and certificate private keys for untrusted apps; SetApplicationName is not isolation from another holder of master keys.", "components": ["net10"], "sources": ["net10:s6682926c8da7"], "status": "REASONED"}, + "cookie-sharing": {"text": "Cookie continuity also needs compatible cookie names, schemes, stores and identity-validation configuration.", "components": ["net10"], "sources": ["net10:s9463626a3d0e"], "status": "REASONED"}, + "server-header": {"text": "NET 10 AddServerHeader defaults true; set false and check all hosting layers since header removal does not control access.", "components": ["source"], "sources": ["source:s18c44d6c2fb7"], "status": "REASONED"}, + "iis-headers": {"text": "Remove IIS X-Powered-By and set removeServerHeader; the latter requires IIS 10 with Windows Server/Windows 10 version 1709 or later.", "components": ["iis", "iis-rolling"], "sources": ["iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED"}, + "openapi": {"text": "NET 10 built-in OpenAPI requires AddOpenApi before Build; map only in Development or RequireAuthorization with an appropriate policy.", "components": ["net10"], "sources": ["net10:s1d58219cedec"], "status": "REASONED"}, + "swagger": {"text": "Built-in OpenAPI has no Swagger UI; keep separately added UseSwaggerUI in Development or protect it separately from the document.", "components": ["net10"], "sources": ["net10:s1f9d4c5e975c"], "status": "REASONED"}, + "health": {"text": "Register health checks and authorize /healthz; monitoring must authenticate or use a deliberately controlled minimal liveness route.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, + "health-host": {"text": "RequireHost checks a spoofable Host header and is not a network or authentication boundary.", "components": ["net10"], "sources": ["net10:sc41b77cc06c8"], "status": "REASONED"}, + "host-filter": {"text": "AllowedHosts uses semicolon-separated names without ports; wildcard permits all. It neither authenticates nor binds interfaces; forwarded-host allowlisting is separate.", "components": ["net10"], "sources": ["net10:s5701d26de241", "net10:se101626c29d6"], "status": "REASONED"}, + "verify-redirect": {"text": "HTTP should return 307/308 with HTTPS Location.", "components": ["docs"], "sources": ["docs:s8c762260991c"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "HTTPS must validate without -k and show HSTS.", "components": ["docs", "net10"], "sources": ["docs:s8c762260991c", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, + "verify-api": {"text": "Known protected NET 10 cookie API endpoints return 401 anonymous and 403 forbidden; pages/OIDC/custom handlers may redirect. Confirm authorized success first.", "components": ["net10"], "sources": ["net10:s837df4b1657f"], "status": "REASONED", "verify": [1]}, + "verify-bind": {"text": "Inspect every listener for 127.0.0.1 and ::1 behind the proxy.", "components": ["docs", "net10"], "sources": ["docs:s74dcd6a72d63", "net10:s74dcd6a72d63"], "status": "REASONED", "verify": [1]}, + "verify-errors": {"text": "Controlled exception should expose marker/detail under development and generic 500 under production; inspect plain/HTML bodies and correlate handler execution.", "components": ["net10"], "sources": ["net10:s2f7e6a8213f1"], "status": "REASONED", "verify": [2]}, + "verify-body": {"text": "Body-consuming test route returns 204 at 1048576 bytes, 413 at 1048577; raised limit permits both. Isolate other limits and distinguish upstream rejection.", "components": ["net10"], "sources": ["net10:s1ce47370d948"], "status": "REASONED", "verify": [3]}, + "verify-aux": {"text": "Probe actual diagnostic/OpenAPI/UI/health routes anonymously, then authorized; Development-only routes should be absent and retained routes protected. Status alone does not prove authorization.", "components": ["net10"], "sources": ["net10:s1d58219cedec", "net10:s1f9d4c5e975c", "net10:sc41b77cc06c8"], "status": "REASONED", "verify": [4]}, + "verify-migrations": {"text": "GET cannot prove migrations middleware absent; inspect registration and test actual behavior only in isolated database infrastructure.", "components": ["net10"], "sources": ["net10:s9d290374f4e8"], "status": "REASONED"}, + "verify-headers": {"text": "Inspect public success, redirect and error responses for absent Server/X-Powered-By after hosting-layer removal where supported.", "components": ["source", "iis", "iis-rolling"], "sources": ["source:s18c44d6c2fb7", "iis-rolling:s267eddd43502", "iis:sb2eac755b583"], "status": "REASONED", "verify": [5]}, + "verify-cookie": {"text": "Anonymous cookie API control returns 401; original cookie keeps the same identity with 200 before/after restart and across confirmed replicas, before expiry and without identity changes.", "components": ["net10"], "sources": ["net10:s837df4b1657f", "net10:s66f1ab4555d6", "net10:s9463626a3d0e"], "status": "REASONED", "verify": [6]}, + "verify-antiforgery": {"text": "Retain an antiforgery token and cookie across restart and submit without replacement; key loss can invalidate it independently of login recovery.", "components": ["net10"], "sources": ["net10:s6682926c8da7", "net10:s66f1ab4555d6"], "status": "REASONED"} + } +} +```` + + ====================================================================== ==> java.md ====================================================================== @@ -16219,92 +17059,9 @@ Claims (full source mapping in the JSON block below): # PHP and Laravel: 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): - -- php: PHP documentation (unknown) -- curl: curl manual (7.75.0) -- php8: PHP native configuration (8.x) -- template84: PHP production template (8.4) -- template83: PHP production template (8.3) -- laravel: Laravel (12.x) -- debugbar: Laravel Debugbar (unknown) -- fail2ban: fail2ban (1.1.1) - -Claims (full source mapping in the front matter): +Written against PHP native configuration 8.x, PHP production template 8.4 and 8.3, Laravel 12.x, fail2ban 1.1.1, and curl manual 7.75.0; 2 further sources carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 65 claims, all reasoned from cited sources. -- fpm-private (REASONED; php8): FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM. -- fpm-clients (REASONED; php8): listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP. -- fpm-socket (REASONED; php8): Unix socket access uses listen.owner, listen.group and listen.mode, default 0660. -- cookie-secure (REASONED; php8): Native session.cookie_secure defaults 0; enable it. -- cookie-httponly (REASONED; php8): Native session.cookie_httponly defaults 0; enable it. -- cookie-samesite (REASONED; php8): Native session.cookie_samesite defaults empty; choose Lax or Strict. -- session-strict (REASONED; php8): session.use_strict_mode defaults 0; enable it. -- session-regenerate (REASONED; php): Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion. -- passwords (REASONED; php): PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes. -- plain-auth (REASONED; php, fail2ban): Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here. -- laravel-debug (REASONED; laravel): Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values. -- laravel-key (REASONED; laravel): Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation. -- laravel-secure (REASONED; laravel): Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default. -- laravel-httponly (REASONED; laravel): SESSION_HTTP_ONLY defaults true; retain it. -- laravel-samesite (REASONED; laravel): SESSION_SAME_SITE defaults lax; strict suits admin-only apps. -- laravel-proxy (REASONED; laravel): Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability. -- laravel-urls (REASONED; laravel): URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs. -- laravel-hashes (REASONED; laravel): Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms. -- laravel-limiter (REASONED; laravel): Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route. -- fortify-login (REASONED; laravel): 12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views. -- fortify-mfa (REASONED; laravel): Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes. -- socialite (REASONED; laravel): Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks. -- laravel-cache (REASONED; laravel): Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files. -- laravel-hosts (REASONED; laravel): Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false. -- debug-tools (REASONED; laravel, debugbar): Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration. -- curl-peer (REASONED; php): CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification. -- curl-host (REASONED; php): CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA. -- display-errors (REASONED; php8): PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running. -- startup-errors (REASONED; php8): display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off. -- log-errors (REASONED; php8): log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger. -- error-reporting (REASONED; php8): error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ. -- php-banner (REASONED; php8, php): expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification. -- production-template (REASONED; template84): PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled. -- disable-functions (REASONED; php8): disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary. -- url-fopen (REASONED; php): allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy. -- url-include (REASONED; php): allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen. -- basedir (REASONED; php8): open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components. -- fpm-extensions (REASONED; php8): PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server. -- upload-execution (REASONED; php8, laravel): Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks. -- laravel-storage (REASONED; laravel): Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution. -- upload-size (REASONED; php8): upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead. -- post-size (REASONED; php8): post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app. -- upload-count (REASONED; php8, php): max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit. -- memory-limit (REASONED; php8): memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound. -- input-vars (REASONED; php8): max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit. -- execution-time (REASONED; php8): max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline. -- input-time (REASONED; php8, template84): max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds. -- upload-time (REASONED; php): Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting. -- fpm-timeout (REASONED; php8): request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers. -- fpm-finished (REASONED; php8): request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work. -- session-gc (REASONED; php8, php): Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes. -- session-lifetime (REASONED; php8, laravel): Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives. -- session-only-cookies (REASONED; php8): session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4. -- session-storage (REASONED; php8): Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities. -- session-sid-length (REASONED; php8, template83): PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26. -- session-sid-bits (REASONED; php8, template83): PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5. -- verify-listener (REASONED; php8): Inventory all FPM listeners: Unix socket or loopback TCP 9000 only. -- verify-tls (REASONED; curl): The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration. -- verify-cookies (REASONED; php8, laravel): Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax. -- verify-auth (REASONED; laravel): Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication. -- verify-config (REASONED; php, php8): Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo. -- verify-banner (REASONED; php): Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection. -- verify-error (REASONED; php8, php): Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested. -- verify-laravel-error (REASONED; laravel): Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors. -- verify-curl-version (REASONED; curl): The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual. - +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. PHP normally runs behind a web server (Apache with php-fpm or mod_php, nginx or Caddy with php-fpm), so TLS terminates there: follow [apache.md](apache.md), [nginx.md](nginx.md), or [caddy.md](caddy.md) with a certificate from [free-certificates.md](free-certificates.md). The PHP-specific exposures are the php-fpm FastCGI socket (the PHP manual: "An exposed FastCGI endpoint allows arbitrary code execution"), session cookies that PHP ships without `Secure`, `HttpOnly`, or `SameSite` (all three default off), `APP_DEBUG=true` left on in production, and cURL calls with certificate checks turned off. @@ -16647,63 +17404,284 @@ REASONED: Laravel exception-route comparison. A live Laravel deployment stand-up - Laravel 12.x Telescope local-only installation: https://laravel.com/framework/docs/12.x/telescope - Laravel Debugbar production-installation guidance: https://github.com/fruitcake/laravel-debugbar -====================================================================== -==> ruby.md -====================================================================== - -# Ruby on Rails and Puma: 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/php.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- puma-readme: Puma README (aef89221e4d729c3133c723844382331ba3bbbd9) -- puma-dsl: Puma DSL (d70de8b4e926f1f5fa0269dc46cdfadf52562628) -- puma: Puma defaults (v8.0.2) -- rails: Rails documentation (unknown) -- assume: Action Pack (7.1) -- credentials: Rails credentials source (e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a) -- rack: Rack::Attack (unknown) -- sidekiq: Sidekiq Web (unknown) -- net-http: Net::HTTP source (39cf5f648a9e5e25312909a2dc26681edaa2402c) -- openssl-ruby: Ruby OpenSSL source (a77ed4b9908e179cea8f018f7e245535301746a8) -- openssl: OpenSSL documentation (unknown) +- php8: PHP native configuration (8.x) +- template84: PHP production template (8.4) +- template83: PHP production template (8.3) +- laravel: Laravel (12.x) +- php: PHP documentation (unknown) +- debugbar: Laravel Debugbar (unknown) +- fail2ban: fail2ban (1.1.1) +- curl: curl manual (7.75.0) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- puma-default (REASONED; puma): Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0. -- puma-private (REASONED; puma-readme, puma-dsl): Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only. -- puma-tls (REASONED; puma-dsl): Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates. -- force-ssl (REASONED; rails): force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS. -- assume-ssl (REASONED; rails, assume): Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP. -- hosts (REASONED; rails): Add the application hostname to config.hosts for Host allowlisting. -- session (REASONED; rails): CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry. -- trusted-proxies (REASONED; rails): Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits. -- same-site-default (REASONED; rails): cookies_same_site_protection defaults lax from load_defaults 6.1. -- auth-generator (REASONED; rails): Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password. -- passwords (REASONED; rails): has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length. -- rate-limit (REASONED; rails): Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store. -- rack-attack (REASONED; rack): Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware. -- credentials (REASONED; credentials, rails): Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores. -- require-key (REASONED; rails): require_master_key=true refuses boot without the decryption key. -- mfa (REASONED; rails): Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance. -- sidekiq-open (REASONED; sidekiq): Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues. -- sidekiq-devise (REASONED; sidekiq): Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth. -- sidekiq-basic (REASONED; sidekiq): Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &. -- client-tls (REASONED; net-http): Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE. -- client-store (REASONED; openssl-ruby, openssl): Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store. -- verify-bind (REASONED; puma-readme, puma-dsl): Inventory every listener; Puma should bind only 127.0.0.1:3000. -- verify-https (REASONED; rails): HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies. -- verify-dashboard (REASONED; rails): Anonymous dashboard request should return login 302 or 401. -- verify-sidekiq (REASONED; sidekiq): Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success. -- verify-key (REASONED; credentials): git ls-files config/master.key should print nothing. +- fpm-private (REASONED; php8): FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM. +- fpm-clients (REASONED; php8): listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP. +- fpm-socket (REASONED; php8): Unix socket access uses listen.owner, listen.group and listen.mode, default 0660. +- cookie-secure (REASONED; php8): Native session.cookie_secure defaults 0; enable it. +- cookie-httponly (REASONED; php8): Native session.cookie_httponly defaults 0; enable it. +- cookie-samesite (REASONED; php8): Native session.cookie_samesite defaults empty; choose Lax or Strict. +- session-strict (REASONED; php8): session.use_strict_mode defaults 0; enable it. +- session-regenerate (REASONED; php): Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion. +- passwords (REASONED; php): PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes. +- plain-auth (REASONED; php, fail2ban): Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here. +- laravel-debug (REASONED; laravel): Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values. +- laravel-key (REASONED; laravel): Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation. +- laravel-secure (REASONED; laravel): Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default. +- laravel-httponly (REASONED; laravel): SESSION_HTTP_ONLY defaults true; retain it. +- laravel-samesite (REASONED; laravel): SESSION_SAME_SITE defaults lax; strict suits admin-only apps. +- laravel-proxy (REASONED; laravel): Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability. +- laravel-urls (REASONED; laravel): URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs. +- laravel-hashes (REASONED; laravel): Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms. +- laravel-limiter (REASONED; laravel): Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route. +- fortify-login (REASONED; laravel): 12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views. +- fortify-mfa (REASONED; laravel): Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes. +- socialite (REASONED; laravel): Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks. +- laravel-cache (REASONED; laravel): Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files. +- laravel-hosts (REASONED; laravel): Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false. +- debug-tools (REASONED; laravel, debugbar): Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration. +- curl-peer (REASONED; php): CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification. +- curl-host (REASONED; php): CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA. +- display-errors (REASONED; php8): PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running. +- startup-errors (REASONED; php8): display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off. +- log-errors (REASONED; php8): log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger. +- error-reporting (REASONED; php8): error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ. +- php-banner (REASONED; php8, php): expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification. +- production-template (REASONED; template84): PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled. +- disable-functions (REASONED; php8): disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary. +- url-fopen (REASONED; php): allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy. +- url-include (REASONED; php): allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen. +- basedir (REASONED; php8): open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components. +- fpm-extensions (REASONED; php8): PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server. +- upload-execution (REASONED; php8, laravel): Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks. +- laravel-storage (REASONED; laravel): Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution. +- upload-size (REASONED; php8): upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead. +- post-size (REASONED; php8): post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app. +- upload-count (REASONED; php8, php): max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit. +- memory-limit (REASONED; php8): memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound. +- input-vars (REASONED; php8): max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit. +- execution-time (REASONED; php8): max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline. +- input-time (REASONED; php8, template84): max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds. +- upload-time (REASONED; php): Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting. +- fpm-timeout (REASONED; php8): request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers. +- fpm-finished (REASONED; php8): request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work. +- session-gc (REASONED; php8, php): Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes. +- session-lifetime (REASONED; php8, laravel): Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives. +- session-only-cookies (REASONED; php8): session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4. +- session-storage (REASONED; php8): Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities. +- session-sid-length (REASONED; php8, template83): PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26. +- session-sid-bits (REASONED; php8, template83): PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5. +- verify-listener (REASONED; php8): Inventory all FPM listeners: Unix socket or loopback TCP 9000 only. +- verify-tls (REASONED; curl): The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration. +- verify-cookies (REASONED; php8, laravel): Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax. +- verify-auth (REASONED; laravel): Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication. +- verify-config (REASONED; php, php8): Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo. +- verify-banner (REASONED; php): Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection. +- verify-error (REASONED; php8, php): Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested. +- verify-laravel-error (REASONED; laravel): Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors. +- verify-curl-version (REASONED; curl): The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "b65232e7b90f8edc69a6b0b2639a04b3f7b6b4590b7015c4d1d84cd70d5f9aae", + "components": { + "php8": { + "name": "PHP native configuration", + "basis": "8.x", + "sources": { + "s89c799077b82": "https://www.php.net/manual/en/errorfunc.configuration.php", + "s5a3f7bde8338": "https://www.php.net/manual/en/info.configuration.php", + "sc9e417e2ad6f": "https://www.php.net/manual/en/ini.core.php", + "s2e692b7c86a6": "https://www.php.net/manual/en/install.fpm.configuration.php", + "s000e155a7b25": "https://www.php.net/manual/en/session.configuration.php" + } + }, + "template84": { + "name": "PHP production template", + "basis": "8.4", + "sources": { + "sd72e3d69551f": "https://raw.githubusercontent.com/php/php-src/d313ad6098430f4e61f0121a9e7ab392d195e4e4/php.ini-production" + } + }, + "template83": { + "name": "PHP production template", + "basis": "8.3", + "sources": { + "sea3d3a4ea486": "https://raw.githubusercontent.com/php/php-src/b94f9f68a610e0fca5f2fea5bfa7c7d6d3d5a847/php.ini-production" + } + }, + "laravel": { + "name": "Laravel", + "basis": "12.x", + "sources": { + "sf0448f3d4665": "https://api.laravel.com/docs/12.x/Illuminate/Routing/UrlGenerator.html", + "s98db3429151c": "https://github.com/laravel/laravel/blob/f6b2e79bdbfc5bf4a37ad16466cc06ad79cc9e8f/config/session.php", + "s4e846bd588a8": "https://laravel.com/framework/docs/12.x/configuration", + "s6510c1aa8d96": "https://laravel.com/framework/docs/12.x/deployment", + "sb3e2d08bd07d": "https://laravel.com/framework/docs/12.x/encryption", + "s879d69d64852": "https://laravel.com/framework/docs/12.x/filesystem", + "sc58ba221e3cd": "https://laravel.com/framework/docs/12.x/fortify", + "sdf4ba04ddcda": "https://laravel.com/framework/docs/12.x/hashing", + "s517839d232ec": "https://laravel.com/framework/docs/12.x/requests", + "s248fef722141": "https://laravel.com/framework/docs/12.x/routing", + "s003198079bdd": "https://laravel.com/framework/docs/12.x/socialite", + "sb3e846095c54": "https://laravel.com/framework/docs/12.x/starter-kits", + "sd7d2287b5d35": "https://laravel.com/framework/docs/12.x/telescope" + } + }, + "php": { + "name": "PHP documentation", + "basis": "unknown", + "sources": { + "s09fdbeba0867": "https://www.php.net/manual/en/curl.constants.php", + "s5840a5a0a848": "https://www.php.net/manual/en/features.commandline.options.php", + "s3cbc65590d52": "https://www.php.net/manual/en/features.file-upload.common-pitfalls.php", + "s6f3e55d536e7": "https://www.php.net/manual/en/filesystem.configuration.php", + "s50f85ff4fbd1": "https://www.php.net/manual/en/function.password-hash.php", + "s625cd1d40655": "https://www.php.net/manual/en/function.session-regenerate-id.php", + "s2f1c806d037c": "https://www.php.net/manual/en/function.trigger-error.php", + "s7b2c2c19835d": "https://www.php.net/manual/en/security.hiding.php", + "s8664dc56a028": "https://www.php.net/manual/en/session.security.ini.php" + } + }, + "debugbar": { + "name": "Laravel Debugbar", + "basis": "unknown", + "sources": { + "s7099781fdf4a": "https://github.com/fruitcake/laravel-debugbar" + } + }, + "fail2ban": { + "name": "fail2ban", + "basis": "1.1.1", + "sources": { + "s0470655e109b": "https://github.com/fail2ban/fail2ban/blob/1.1.1/man/jail.conf.5#L187-L200", + "s9b3b926fe020": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/jail.conf#L378-L384", + "sdb5f6f355db6": "https://github.com/fail2ban/fail2ban/blob/1.1.1/config/filter.d/nginx-limit-req.conf#L48-L49" + } + }, + "curl": { + "name": "curl manual", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + } + }, + "claims": { + "fpm-private": {"text": "FPM requires listen per pool; exposed FastCGI permits code execution. Use a Unix socket or loopback TCP 9000; never publish container FPM.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-clients": {"text": "listen.allowed_clients defaults unset, accepting any address; explicitly allow loopback for TCP.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-socket": {"text": "Unix socket access uses listen.owner, listen.group and listen.mode, default 0660.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "cookie-secure": {"text": "Native session.cookie_secure defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "cookie-httponly": {"text": "Native session.cookie_httponly defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "cookie-samesite": {"text": "Native session.cookie_samesite defaults empty; choose Lax or Strict.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-strict": {"text": "session.use_strict_mode defaults 0; enable it.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-regenerate": {"text": "Regenerate IDs after login/privilege changes; delete_old_session defaults false. Timestamp expiry avoids immediate deletion.", "components": ["php"], "sources": ["php:s625cd1d40655"], "status": "REASONED"}, + "passwords": {"text": "PASSWORD_DEFAULT currently uses bcrypt; Argon2id needs an Argon2 build. Verify/rehash passwords and allow growing hashes, suggested 255 bytes.", "components": ["php"], "sources": ["php:s50f85ff4fbd1"], "status": "REASONED"}, + "plain-auth": {"text": "Guide assessment: the cited PHP password and session functions provide no login-attempt throttling; this is not a claim about every PHP extension or application. Guide advice: apply a proxy limiter or a fail2ban jail over an application/proxy failure log, following authentication.md requirements. nginx.md section 4 documents limit_req; caddy.md section 4 states that stock Caddy has no rate limiter; apache.md gives no limiter configuration. fail2ban 1.1.1 documents filter and logpath; its nginx-limit-req jail reads the nginx error log and its filter matches limiting or delaying entries. These sources do not establish a filter for this PHP application's login form or its log format. Guide advice: for plain-PHP MFA, use a TOTP library or an identity layer in front whose policy requires MFA; mfa.md covers identity-layer options and enforcement requirements, but its library list has no PHP TOTP library. No PHP TOTP library is pinned here.", "components": ["php", "fail2ban"], "sources": ["php:s50f85ff4fbd1", "php:s8664dc56a028", "fail2ban:s0470655e109b", "fail2ban:s9b3b926fe020", "fail2ban:sdb5f6f355db6"], "status": "REASONED"}, + "laravel-debug": {"text": "Set APP_ENV=production, APP_DEBUG=false and HTTPS APP_URL; debug mode exposes configuration values.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, + "laravel-key": {"text": "Generate APP_KEY with artisan key:generate; list old keys in APP_PREVIOUS_KEYS during rotation.", "components": ["laravel"], "sources": ["laravel:sb3e2d08bd07d"], "status": "REASONED"}, + "laravel-secure": {"text": "Set SESSION_SECURE_COOKIE=true; the session secure configuration has no default.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-httponly": {"text": "SESSION_HTTP_ONLY defaults true; retain it.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-samesite": {"text": "SESSION_SAME_SITE defaults lax; strict suits admin-only apps.", "components": ["laravel"], "sources": ["laravel:s98db3429151c"], "status": "REASONED"}, + "laravel-proxy": {"text": "Laravel 12.x trustProxies names proxies in bootstrap/app.php; wildcard trust requires exclusive proxy reachability.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, + "laravel-urls": {"text": "URL::forceHttps() or forceScheme(https) in a provider boot method forces generated HTTPS URLs.", "components": ["laravel"], "sources": ["laravel:sf0448f3d4665"], "status": "REASONED"}, + "laravel-hashes": {"text": "Hash::make/check default to bcrypt; HASH_DRIVER=argon2id switches it and needsRehash detects upgrades. HASH_VERIFY=false disables rejection of other algorithms.", "components": ["laravel"], "sources": ["laravel:sdf4ba04ddcda"], "status": "REASONED"}, + "laravel-limiter": {"text": "Define a five-per-minute login limiter keyed by email plus IP; attach throttle:login to the real route.", "components": ["laravel"], "sources": ["laravel:s248fef722141"], "status": "REASONED"}, + "fortify-login": {"text": "12.x starter kits use Fortify, which throttles username plus IP and regenerates session IDs on login; standalone Fortify omits views.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, + "fortify-mfa": {"text": "Fortify twoFactorAuthentication with confirm and confirmPassword supplies TOTP and recovery codes.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd", "laravel:sb3e846095c54"], "status": "REASONED"}, + "socialite": {"text": "Socialite supplies OAuth providers; community packages supply OIDC. Use HTTPS callbacks and linked allowlist/token checks.", "components": ["laravel"], "sources": ["laravel:s003198079bdd"], "status": "REASONED"}, + "laravel-cache": {"text": "Keep plaintext .env untracked; build config/route caches after production inputs and rebuild on changes. Cached config does not load .env; use env only in config files.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED"}, + "laravel-hosts": {"text": "Laravel accepts arbitrary Host headers by default; restrict hosts at the server and add anchored TrustHosts with subdomains:false.", "components": ["laravel"], "sources": ["laravel:s517839d232ec"], "status": "REASONED"}, + "debug-tools": {"text": "Omit Telescope and Debugbar from production; Telescope local-only installation includes conditional provider registration.", "components": ["laravel", "debugbar"], "sources": ["laravel:sd7d2287b5d35", "debugbar:s7099781fdf4a"], "status": "REASONED"}, + "curl-peer": {"text": "CURLOPT_SSL_VERIFYPEER defaults true; retain certificate verification.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, + "curl-host": {"text": "CURLOPT_SSL_VERIFYHOST defaults 2; retain hostname checks and use CURLOPT_CAINFO for an internal CA.", "components": ["php"], "sources": ["php:s09fdbeba0867"], "status": "REASONED"}, + "display-errors": {"text": "PHP 8.x display_errors defaults On; set Off before execution. ini_set cannot hide a fatal error preventing it from running.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "startup-errors": {"text": "display_startup_errors defaults On since PHP 8.0.0, previously Off; set Off.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "log-errors": {"text": "log_errors defaults Off; enable it with a protected, pre-created, worker-writable log outside the document root and rotation. Unset error_log uses the SAPI logger.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "error-reporting": {"text": "error_reporting defaults E_ALL since PHP 8.0.0; retain reporting into logs. Package/template/deployment overrides can differ.", "components": ["php8"], "sources": ["php8:s89c799077b82"], "status": "REASONED"}, + "php-banner": {"text": "expose_php defaults On; Off suppresses PHP own X-Powered-By header, not other layers or all application identification.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s7b2c2c19835d"], "status": "REASONED"}, + "production-template": {"text": "PHP 8.4 production template disables display and enables logging, but uses E_ALL & ~E_DEPRECATED and leaves expose_php enabled.", "components": ["template84"], "sources": ["template84:sd72e3d69551f"], "status": "REASONED"}, + "disable-functions": {"text": "disable_functions defaults empty; test the listed process-function restrictions. Since PHP 8.0 definitions disappear and userland can redefine them; internal functions only, not a security boundary.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "url-fopen": {"text": "allow_url_fopen defaults On; disable unnecessary URL wrappers after compatibility tests, not as a general egress policy.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, + "url-include": {"text": "allow_url_include defaults Off, deprecated since PHP 7.4; retain Off. URL includes also require allow_url_fopen.", "components": ["php"], "sources": ["php:s6f3e55d536e7"], "status": "REASONED"}, + "basedir": {"text": "open_basedir defaults NULL; allow required paths, retain OS isolation and account for disabled realpath caching. PHP 8.3 rejects runtime .. components.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "fpm-extensions": {"text": "PHP 8.x FPM security.limit_extensions defaults .php .phar; restrict to .php, which still permits uploaded PHP dispatched by the server.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "upload-execution": {"text": "Separate writable uploads from code, preferably outside the document root; server mappings must forbid PHP execution, including aliases/symlinks.", "components": ["php8", "laravel"], "sources": ["php8:s2e692b7c86a6", "laravel:s879d69d64852"], "status": "REASONED"}, + "laravel-storage": {"text": "Laravel 12.x local disk defaults storage/app/private; public disk uses storage/app/public via public/storage, which does not itself prevent PHP execution.", "components": ["laravel"], "sources": ["laravel:s879d69d64852"], "status": "REASONED"}, + "upload-size": {"text": "upload_max_filesize defaults 2M per file; post_max_size must allow multipart overhead.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "post-size": {"text": "post_max_size defaults 8M; excess leaves POST and FILES empty and must be rejected by the app.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "upload-count": {"text": "max_file_uploads defaults 20; twenty maximum-sized files need not fit the POST limit.", "components": ["php8", "php"], "sources": ["php8:sc9e417e2ad6f", "php:s3cbc65590d52"], "status": "REASONED"}, + "memory-limit": {"text": "memory_limit defaults 128M, should generally exceed post_max_size, and -1 removes the bound.", "components": ["php8"], "sources": ["php8:sc9e417e2ad6f"], "status": "REASONED"}, + "input-vars": {"text": "max_input_vars defaults 1000 separately for GET, POST and COOKIE; excess is warned/truncated, not a general JSON limit.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, + "execution-time": {"text": "max_execution_time defaults 30 for web and 0 for CLI; platform/build accounting can exclude I/O, not a portable wall-clock deadline.", "components": ["php8"], "sources": ["php8:s5a3f7bde8338"], "status": "REASONED"}, + "input-time": {"text": "max_input_time defaults -1, using max_execution_time; 0 is unlimited. Example and production template select 60 seconds.", "components": ["php8", "template84"], "sources": ["php8:s5a3f7bde8338", "template84:sd72e3d69551f"], "status": "REASONED"}, + "upload-time": {"text": "Test slow/multiple uploads and coordinate server size/time limits. Per-request bounds do not replace capacity controls or rate limiting.", "components": ["php"], "sources": ["php:s3cbc65590d52"], "status": "REASONED"}, + "fpm-timeout": {"text": "request_terminate_timeout defaults 0, disabled; example selects 60s as app policy and kills overlong workers.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "fpm-finished": {"text": "request_terminate_timeout_track_finished defaults no; yes extends the limit past fastcgi_finish_request and into shutdown work.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED"}, + "session-gc": {"text": "Native gc_maxlifetime defaults 1440 seconds; probabilistic collection is not auth expiry. Use timestamp expiry/cleanup; shared paths can inherit shorter lifetimes.", "components": ["php8", "php"], "sources": ["php8:s000e155a7b25", "php:s8664dc56a028"], "status": "REASONED"}, + "session-lifetime": {"text": "Native cookie_lifetime defaults 0, a browser-session cookie; Laravel uses config/session.php rather than native storage/lifetime directives.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED"}, + "session-only-cookies": {"text": "session.use_only_cookies defaults 1; retain it to exclude GET/POST IDs. Disabling it is deprecated from PHP 8.4.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-storage": {"text": "Default files handler needs a private worker-writable directory outside the document root; dedicated-account mode 0700, default file mode 0600, separate untrusted-app identities.", "components": ["php8"], "sources": ["php8:s000e155a7b25"], "status": "REASONED"}, + "session-sid-length": {"text": "PHP 8.x session.sid_length defaults 32; changes are deprecated from 8.4. PHP 8.3 production template sets 26.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, + "session-sid-bits": {"text": "PHP 8.x session.sid_bits_per_character defaults 4; changes are deprecated from 8.4. PHP 8.3 production template sets 5.", "components": ["php8", "template83"], "sources": ["php8:s000e155a7b25", "template83:sea3d3a4ea486"], "status": "REASONED"}, + "verify-listener": {"text": "Inventory all FPM listeners: Unix socket or loopback TCP 9000 only.", "components": ["php8"], "sources": ["php8:s2e692b7c86a6"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "The TLS probe distinguishes cited exit=60 certificate failure from reasoned exit=0 success without -k; linked fronting-server guides supply TLS configuration.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-cookies": {"text": "Inspect login Set-Cookie for Secure, HttpOnly and SameSite=Lax.", "components": ["php8", "laravel"], "sources": ["php8:s000e155a7b25", "laravel:s98db3429151c"], "status": "REASONED", "verify": [1]}, + "verify-auth": {"text": "Anonymous protected routes must deny or redirect to login without protected content; require authorized success and isolated auth-removal control. Unrelated redirects/errors and anonymous Set-Cookie do not establish authentication.", "components": ["laravel"], "sources": ["laravel:sc58ba221e3cd"], "status": "REASONED", "verify": [2]}, + "verify-config": {"text": "Inspect CLI ini/settings against installed E_ALL; CLI does not establish web SAPI/FPM settings. Do not publish phpinfo.", "components": ["php", "php8"], "sources": ["php:s5840a5a0a848", "php8:s89c799077b82", "php8:s2e692b7c86a6"], "status": "REASONED", "verify": [3]}, + "verify-banner": {"text": "Fixture should give HTTP 200; expose_php On should show PHP header unless stripped, Off must hide it. Proxy stripping in both states needs SAPI inspection.", "components": ["php"], "sources": ["php:s7b2c2c19835d", "php:s2f1c806d037c"], "status": "REASONED", "verify": [4]}, + "verify-error": {"text": "Native warning fixture must disclose canary with display_errors On, then return reached marker without diagnostics and log the warning after hardening. Errors/missing logs/blank bodies are inconclusive; startup/parse errors untested.", "components": ["php8", "php"], "sources": ["php8:s89c799077b82", "php:s2f1c806d037c"], "status": "REASONED", "verify": [5]}, + "verify-laravel-error": {"text": "Repeat body probe with controlled Laravel exception, rebuild config cache after APP_DEBUG changes, and require generic response plus protected log; native fixture does not test framework errors.", "components": ["laravel"], "sources": ["laravel:s4e846bd588a8", "laravel:s6510c1aa8d96"], "status": "REASONED", "verify": [5]}, + "verify-curl-version": {"text": "The exitcode and errormsg write-out variables require curl 7.75.0 or later per the curl manual.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "REASONED", "verify": [1, 2, 4, 5]} + } +} +```` +====================================================================== +==> ruby.md +====================================================================== + +# Ruby on Rails and Puma: TLS and authentication + +Written against Rails documentation (no recorded version), Action Pack 7.1, and Puma defaults v8.0.2; 5 further sources are pinned to a commit and 3 carry no recorded version. Metadata reviewed 2026-09-26; 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. + Puma's default bind is `tcp://[::]:9292` when the host has a non-loopback IPv6 address, otherwise `tcp://0.0.0.0:9292` (as of v8.0.2): every interface, plain HTTP. Rails encrypts its session cookie but still sends it in clear unless HTTPS is enforced. Preferred layout: bind Puma to loopback and terminate TLS in a reverse proxy ([nginx.md](nginx.md), [caddy.md](caddy.md), [apache.md](apache.md)) or behind [cloudflare.md](cloudflare.md), with a certificate from [free-certificates.md](free-certificates.md). Puma can also terminate TLS itself, shown below. ## 1. Bind Puma privately @@ -16830,6 +17808,201 @@ git ls-files config/master.key # prints nothin - Ruby OpenSSL `SSLContext` defaults (`DEFAULT_CERT_STORE.set_default_paths`, `VERIFY_PEER`): https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb - OpenSSL environment variables (`SSL_CERT_FILE`, `SSL_CERT_DIR`): https://docs.openssl.org/master/man7/openssl-env/ + + +## 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/ruby.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): + +- rails: Rails documentation (unknown) +- assume: Action Pack (7.1) +- puma: Puma defaults (v8.0.2) +- puma-readme: Puma README (aef89221e4d729c3133c723844382331ba3bbbd9) +- puma-dsl: Puma DSL (d70de8b4e926f1f5fa0269dc46cdfadf52562628) +- credentials: Rails credentials source (e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a) +- rack: Rack::Attack (unknown) +- sidekiq: Sidekiq Web (unknown) +- net-http: Net::HTTP source (39cf5f648a9e5e25312909a2dc26681edaa2402c) +- openssl-ruby: Ruby OpenSSL source (a77ed4b9908e179cea8f018f7e245535301746a8) +- openssl: OpenSSL documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- puma-default (REASONED; puma): Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0. +- puma-private (REASONED; puma-readme, puma-dsl): Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only. +- puma-tls (REASONED; puma-dsl): Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates. +- force-ssl (REASONED; rails): force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS. +- assume-ssl (REASONED; rails, assume): Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP. +- hosts (REASONED; rails): Add the application hostname to config.hosts for Host allowlisting. +- session (REASONED; rails): CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry. +- trusted-proxies (REASONED; rails): Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits. +- same-site-default (REASONED; rails): cookies_same_site_protection defaults lax from load_defaults 6.1. +- auth-generator (REASONED; rails): Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password. +- passwords (REASONED; rails): has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length. +- rate-limit (REASONED; rails): Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store. +- rack-attack (REASONED; rack): Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware. +- credentials (REASONED; credentials, rails): Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores. +- require-key (REASONED; rails): require_master_key=true refuses boot without the decryption key. +- mfa (REASONED; rails): Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance. +- sidekiq-open (REASONED; sidekiq): Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues. +- sidekiq-devise (REASONED; sidekiq): Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth. +- sidekiq-basic (REASONED; sidekiq): Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &. +- client-tls (REASONED; net-http): Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE. +- client-store (REASONED; openssl-ruby, openssl): Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store. +- verify-bind (REASONED; puma-readme, puma-dsl): Inventory every listener; Puma should bind only 127.0.0.1:3000. +- verify-https (REASONED; rails): HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies. +- verify-dashboard (REASONED; rails): Anonymous dashboard request should return login 302 or 401. +- verify-sidekiq (REASONED; sidekiq): Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success. +- verify-key (REASONED; credentials): git ls-files config/master.key should print nothing. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "d7d88bbf754652ad64cd1044824c2cc41633732451a815523e2adabc25c7a2a4", + "components": { + "rails": { + "name": "Rails documentation", + "basis": "unknown", + "sources": { + "s0e5c35fd756d": "https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html", + "s22fb0dcbb6e1": "https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html", + "sba5f72c7a627": "https://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html", + "sae7bf5d75809": "https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html", + "s574629d873e4": "https://guides.rubyonrails.org/configuring.html", + "s2abb8f0ec8d2": "https://guides.rubyonrails.org/security.html" + } + }, + "assume": { + "name": "Action Pack", + "basis": "7.1", + "sources": { + "sf720dd92b2e7": "https://github.com/rails/rails/blob/ffcbf6f205363f8c2fb3e9834bc86690dd59f1cb/actionpack/CHANGELOG.md" + } + }, + "puma": { + "name": "Puma defaults", + "basis": "v8.0.2", + "sources": { + "s9687194339f3": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L369-L384", + "s8e2a34b121ba": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/configuration.rb#L174", + "s1d9011be835b": "https://github.com/puma/puma/blob/v8.0.2/lib/puma/const.rb#L214-L215" + } + }, + "puma-readme": { + "name": "Puma README", + "basis": "aef89221e4d729c3133c723844382331ba3bbbd9", + "sources": { + "se5323687b58f": "https://github.com/puma/puma/blob/aef89221e4d729c3133c723844382331ba3bbbd9/README.md" + } + }, + "puma-dsl": { + "name": "Puma DSL", + "basis": "d70de8b4e926f1f5fa0269dc46cdfadf52562628", + "sources": { + "s8d510226544c": "https://github.com/puma/puma/blob/d70de8b4e926f1f5fa0269dc46cdfadf52562628/lib/puma/dsl.rb" + } + }, + "credentials": { + "name": "Rails credentials source", + "basis": "e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a", + "sources": { + "saab0a90edc0c": "https://github.com/rails/rails/blob/e4cd6ae6f1f0a847958b3aa846c9fd8a3014922a/railties/lib/rails/commands/credentials/USAGE" + } + }, + "rack": { + "name": "Rack::Attack", + "basis": "unknown", + "sources": { + "s1e5ca83efb79": "https://github.com/rack/rack-attack" + } + }, + "sidekiq": { + "name": "Sidekiq Web", + "basis": "unknown", + "sources": { + "s106d0b1ced6e": "https://github.com/sidekiq/sidekiq/wiki/Monitoring" + } + }, + "net-http": { + "name": "Net::HTTP source", + "basis": "39cf5f648a9e5e25312909a2dc26681edaa2402c", + "sources": { + "sbcafa3e06a73": "https://github.com/ruby/net-http/blob/39cf5f648a9e5e25312909a2dc26681edaa2402c/lib/net/http.rb" + } + }, + "openssl-ruby": { + "name": "Ruby OpenSSL source", + "basis": "a77ed4b9908e179cea8f018f7e245535301746a8", + "sources": { + "sf0192683941c": "https://github.com/ruby/openssl/blob/a77ed4b9908e179cea8f018f7e245535301746a8/lib/openssl/ssl.rb" + } + }, + "openssl": { + "name": "OpenSSL documentation", + "basis": "unknown", + "sources": { + "se3858048f9d6": "https://docs.openssl.org/master/man7/openssl-env/" + } + } + }, + "claims": { + "puma-default": {"text": "Puma v8.0.2 defaults plaintext 9292 on :: when non-loopback IPv6 exists, otherwise 0.0.0.0.", "components": ["puma"], "sources": ["puma:s9687194339f3", "puma:s8e2a34b121ba", "puma:s1d9011be835b"], "status": "REASONED"}, + "puma-private": {"text": "Bind loopback TCP 3000 or Unix socket with proxy TLS; bind accepts tcp, unix and ssl URIs only.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED"}, + "puma-tls": {"text": "Native ssl bind example uses 8443 with key/cert; ca and verify_mode configure client certificates.", "components": ["puma-dsl"], "sources": ["puma-dsl:s8d510226544c"], "status": "REASONED"}, + "force-ssl": {"text": "force_ssl enables HTTPS redirect and HSTS; ssl_options defaults hsts subdomains true. Encrypted cookies still need HTTPS.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED"}, + "assume-ssl": {"text": "Rails 7.1+ assume_ssl says the proxy terminated TLS before forwarding HTTP.", "components": ["rails", "assume"], "sources": ["rails:s574629d873e4", "assume:sf720dd92b2e7"], "status": "REASONED"}, + "hosts": {"text": "Add the application hostname to config.hosts for Host allowlisting.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "session": {"text": "CookieStore example selects Secure, HttpOnly, SameSite=Lax and 14-day expiry.", "components": ["rails"], "sources": ["rails:sba5f72c7a627", "rails:s574629d873e4"], "status": "REASONED"}, + "trusted-proxies": {"text": "Defaults include loopback/private/link-local; public proxies require an enumerable. Without a proxy, client-controlled X-Forwarded-For makes remote_ip unsuitable for allowlists/rate limits.", "components": ["rails"], "sources": ["rails:s22fb0dcbb6e1"], "status": "REASONED"}, + "same-site-default": {"text": "cookies_same_site_protection defaults lax from load_defaults 6.1.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "auth-generator": {"text": "Rails 8.0+ authentication generator supplies users, sessions and password reset using has_secure_password.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "passwords": {"text": "has_secure_password needs bcrypt ~> 3.1.7 and password_digest; authenticate returns user/false, validates presence and 72-byte limit, and needs an app minimum length.", "components": ["rails"], "sources": ["rails:sae7bf5d75809"], "status": "REASONED"}, + "rate-limit": {"text": "Controller rate_limit allows ten attempts per three minutes for create; needs ActiveSupport::Cache, defaulting to action_controller.cache_store.", "components": ["rails"], "sources": ["rails:s0e5c35fd756d"], "status": "REASONED"}, + "rack-attack": {"text": "Rack::Attack supplies path throttles/blocklists; configure its initializer and Rails railtie installs middleware.", "components": ["rack"], "sources": ["rack:s1e5ca83efb79"], "status": "REASONED"}, + "credentials": {"text": "Edit encrypted credentials with credentials:edit; RAILS_MASTER_KEY overrides master.key, which Rails gitignores.", "components": ["credentials", "rails"], "sources": ["credentials:saab0a90edc0c", "rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "require-key": {"text": "require_master_key=true refuses boot without the decryption key.", "components": ["rails"], "sources": ["rails:s574629d873e4"], "status": "REASONED"}, + "mfa": {"text": "Rails has no built-in second factor; add TOTP or an identity layer and use linked MFA/OIDC guidance.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED"}, + "sidekiq-open": {"text": "Bare /sidekiq mount has no auth; reachable users can read job arguments and retry, kill or clear queues.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "sidekiq-devise": {"text": "Devise authenticate constraint requires a signed-in admin; keep Sidekiq off the public internet even with auth.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "sidekiq-basic": {"text": "Otherwise add Rack Basic auth with credentials outside source, equal-length SHA256 secure_compare inputs and non-short-circuit &.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED"}, + "client-tls": {"text": "Net::HTTP.start use_ssl defaults VERIFY_PEER; supply ca_file before handshake, not inside the opened block. Never use VERIFY_NONE.", "components": ["net-http"], "sources": ["net-http:sbcafa3e06a73"], "status": "REASONED"}, + "client-store": {"text": "Ruby SSLContext loads system trust through set_default_paths; SSL_CERT_FILE and SSL_CERT_DIR select an internal CA store.", "components": ["openssl-ruby", "openssl"], "sources": ["openssl-ruby:sf0192683941c", "openssl:se3858048f9d6"], "status": "REASONED"}, + "verify-bind": {"text": "Inventory every listener; Puma should bind only 127.0.0.1:3000.", "components": ["puma-readme", "puma-dsl"], "sources": ["puma-readme:se5323687b58f", "puma-dsl:s8d510226544c"], "status": "REASONED", "verify": [1]}, + "verify-https": {"text": "HTTP should redirect 301 to HTTPS; inspect HSTS and Secure/HttpOnly/SameSite=Lax cookies.", "components": ["rails"], "sources": ["rails:s574629d873e4", "rails:sba5f72c7a627"], "status": "REASONED", "verify": [1]}, + "verify-dashboard": {"text": "Anonymous dashboard request should return login 302 or 401.", "components": ["rails"], "sources": ["rails:s2abb8f0ec8d2"], "status": "REASONED", "verify": [1]}, + "verify-sidekiq": {"text": "Basic needs 401 plus WWW-Authenticate, Devise login 302; bare 404 is inconclusive, dashboard 200 is exposure. Require authorized success.", "components": ["sidekiq"], "sources": ["sidekiq:s106d0b1ced6e"], "status": "REASONED", "verify": [1]}, + "verify-key": {"text": "git ls-files config/master.key should print nothing.", "components": ["credentials"], "sources": ["credentials:saab0a90edc0c"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> host.md ====================================================================== @@ -17638,30 +18811,178 @@ Claims (full source mapping in the JSON block below): # PaaS platforms: what the platform does, what stays yours +Written against Render documentation (no recorded version), Next.js pinned documentation v16.4.2, Next.js environment variables 15, Express request protocol 5.x, Django proxy security setting 6.1, and MySQL client connection options 8.4; 3 further sources are pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 22 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 responsibilities depend on the platform and deployment mode: + +- Render: the load balancer terminates HTTPS for web services and forwards HTTP to the app. Render automatically issues and renews certificates for added custom domains, including wildcards. Add the domain, configure DNS, and verify it in the dashboard. Wildcards require the root domain to point to Render and the prescribed wildcard, `_acme-challenge`, and `_cf-custom-hostname` CNAME records; existing CAA records must allow Let's Encrypt and Google Trust Services, with `issuewild` records for wildcards. At the time of writing, included custom-domain quotas depend on the workspace plan and additional domains cost extra. +- Fly.io: Fly Proxy's `tls` handler terminates TLS and forwards plaintext; without handlers, TCP passes through and the app can terminate TLS itself. Add the custom domain and configure DNS for automatic certificate issuance. Issuance and renewal require domain verification through an AAAA record pointing to the app, an `_acme-challenge` CNAME, or a `_fly-ownership` TXT record; wildcard certificates require DNS-01 with the `_acme-challenge` CNAME. These passages describe managed certificates and renewal prerequisites but do not explicitly establish automatic renewal. Imported certificates require ownership verification and take priority over a coexisting Fly-managed fallback. +- Railway: correctly configured custom domains receive automatically issued Let's Encrypt certificates, renewed with 30 days of their 90-day validity remaining; external certificates are unsupported. Both the supplied CNAME and ownership TXT records are required; wildcards also require the `_acme-challenge` CNAME, unproxied on Cloudflare. Root domains require CNAME flattening or dynamic ALIAS records. At the time of writing, custom-domain limits are one on Trial, two per service on Hobby and twenty per service by default on Pro, with increases available on request; Freenom domains are unsupported. For Cloudflare-proxied domains, issuance can fail and Railway can instead serve its default `*.up.railway.app` certificate; the docs require Full mode and warn that Full (Strict) will not work as intended. Cloudflare proxying below first-level subdomains requires Advanced Certificate Manager or switching those records to DNS Only. The cited passages establish Railway's HTTPS certificate handling, but not the precise TLS termination component. +- Vercel: its CDN terminates TLS. Certificates for deployment URLs are generated and kept updated automatically. Custom-domain issuance requires successful validation after DNS propagation; non-wildcards use HTTP-01 reachable at Vercel, and wildcards require Vercel nameservers for DNS-01. Vercel automatically attempts renewal 14 to 30 days before expiry; correct DNS, proxy, and CAA configuration must still permit validation. At the time of writing, only Enterprise teams can configure custom SSL; uploaded certificates cannot be renewed automatically by Vercel. The `/.well-known` path is reserved and cannot be redirected or rewritten. +- Heroku: the HTTP routing page describes load-balancer SSL termination, with separate Private Spaces routers. At the time of writing, ACM is included free for all applications; enable it for automatic issuance and renewal one month before expiry, using the correct Heroku DNS target. Adding custom domains requires a verified Heroku account. Cedar Private Spaces require a running web dyno for validation and renewal, cannot use ACM with Trusted IPs blocking access, and cannot use ACM for wildcard domains. The page says customers must upload their own wildcard certificate for Private Space apps, but its limitations also say "For Generation Private Space apps" can use ACM with wildcards; Fir wildcard eligibility is therefore not established consistently here. Uploading a certificate disables ACM. CAA must permit Let's Encrypt, and DNS/CDN configuration must allow validation; ACM must be disabled if the app requires the platform-handled ACME challenge paths. + +This guide's advice: where the platform terminates TLS and manages the required certificate, do not bolt certbot or an extra TLS reverse proxy onto the app, and do not apply this repository's server-TLS guides to that managed ingress. What stays yours: + +## 1. Authentication: entirely yours + +The platform does not authenticate your application's users. Some platforms protect the deployment itself: Vercel Deployment Protection, for example, can require a Vercel login to open preview and deployment URLs on every plan, and production domains too once the All Deployments scope is selected, which Vercel's changelog of 9 September 2026 made free on every plan including Hobby; Password Protection is not available on Hobby, is a paid add-on per protected project on Pro, and is included on Enterprise (at the time of writing). That gate keeps the public out of a preview; it is not your app's login. Every non-public endpoint still needs login or keys per [authentication.md](authentication.md), MFA where viable per [mfa.md](mfa.md), and a hosted provider makes both easy ([identity-providers.md](identity-providers.md)). "It is on Vercel" changes nothing about an open `/api/admin`. + +## 2. Secrets: use the platform's store + +Each platform provides environment/secret configuration. Set secrets there; never commit `.env` files ([secrets.md](secrets.md)). Rotate anything that ever appeared in the repository, build logs, or client bundles. Public frontend frameworks compile some env vars into the client (for example `NEXT_PUBLIC_`-prefixed values); only put genuinely public values in those. + +## 3. Enforce HTTPS and correct proxy awareness + +- Redirect HTTP to HTTPS. This guide's advice: use the documented automatic redirect, enable the documented platform setting, or implement the redirect in the app with verified proxy trust: + - Render's TLS page says all HTTP requests are automatically redirected to HTTPS; it does not state a status code. + - Fly.io's pinned configuration reference documents `force_https = true` under `[http_service]` in `fly.toml` for apps managed by Fly Launch. The pinned services page says enabling `force_https` returns 301 for all HTTP requests; the option is only valid on HTTP handlers, and deployment fails if it is set on other handlers. + - Railway's pinned public-networking specifications describe automatic HTTPS redirects for plain HTTP GET requests with 301, and HTTP POST redirects that convert the request to GET. They do not establish a configurable redirect toggle, a POST status code, or the HEAD response used below. + - Vercel's encryption page says its CDN automatically redirects HTTP requests to HTTPS with 308 and that this redirection cannot be disabled. + - Heroku's cited HTTP routing page does not establish automatic HTTPS redirection, a redirect toggle, an application-only redirect contract, or a status code. This guide's advice: implement application-side redirection unless the deployed ingress is known to enforce it, and verify the result. + + For application-side handling, this guide's advice is to check the platform's forwarded-protocol header only after verifying the framework's proxy trust requirements. Fly's HTTP handler documents `X-Forwarded-Proto` as the original client protocol, `http` or `https`; Railway's public-networking specifications say it always indicates `https`. Heroku's routing page describes it as the originating protocol but explicitly warns that forwarded headers, including `X-Forwarded-Proto`, are not trusted because the order of pre-existing fields is unknown. These descriptions do not establish header sanitization; the cited Render and Vercel TLS pages do not establish a forwarded-protocol header contract. Keep the Express 5.x `trust proxy` and Django 6.1 `SECURE_PROXY_SSL_HEADER` trust requirements in view when implementing this guide's advice. +- Behind the platform proxy, configure the framework accordingly (`trust proxy` in Express per [nodejs.md](nodejs.md), `SECURE_PROXY_SSL_HEADER` in Django per [python.md](python.md)) so secure cookies and redirects behave. +- Bind the listener required by the deployment model. Render web services must bind `0.0.0.0`; Render recommends `PORT` (default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound. Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the service's configured `internal_port` to the app listener; IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime. Railway domains route to a detected or selected internal target port; the cited pages do not establish an injected `PORT` or required listener address. Heroku assigns web processes a `PORT` environment variable; for Fir, the Generations page says Private Spaces networking uses IPv6 by default, that IPv4-only assumptions do not bind properly, and recommends an IPv6 or dual-stack listener (typically `::`); the cited pages do not establish Cedar's listener address. Vercel runtimes transform code into Functions served by its CDN, including Node.js function entrypoints and Python ASGI/WSGI apps; that page establishes neither a universal injected port nor a required listener address. This guide's advice: bind nothing else and do not open extra listeners. + +## 4. Databases attached to PaaS apps + +Managed databases from these platforms come with TLS endpoints, but check what the specific endpoint supports: require certificate and hostname verification with the client's own setting (`sslmode=verify-full` for PostgreSQL, `--ssl-mode=VERIFY_IDENTITY` for MySQL) per the database guides ([postgresql.md](postgresql.md), [mysql.md](mysql.md)) where the endpoint allows it, and note that some managed internal endpoints support encryption but not `verify-full` (Render's internal PostgreSQL connections, where TLS is optional, do not support `verify-ca`/`verify-full`); there, set `sslmode=require` to force encryption and prevent a plaintext fallback, and do not treat that encryption-only connection as identity-verified. Keep the credentials in the platform's secret store, and restrict the network too: many managed databases are reachable from any IP by default (Render's Postgres is), so prefer private connectivity and disable or source-restrict external access with the provider's database network controls, not TLS alone. Databases you run yourself elsewhere follow their own guides plus [cloud-firewalls.md](cloud-firewalls.md). + +## 5. Hugging Face Spaces + +This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only. Visibility can be changed to "protected" (the source stays private to the owner and collaborators, but the running app is still public through its embed URL or custom domain) or "private" (both the source and the running app are restricted to the owner and collaborators); protected visibility requires a paid plan, PRO for personal accounts or Team/Enterprise for organizations, at the time of writing. Space secrets belong in the Settings tab's secrets store, never in the repository or its README metadata; anything set as a "variable" instead of a "secret" is still publicly readable. That secrets store is not a safe place for backend or provider credentials on a Static Space: Hugging Face exposes both variables and secrets to client-side JavaScript there, through `window.huggingface.variables`, so anything placed in a Static Space's secrets still reaches the browser. Keep server-side credentials in a server-side service, such as a Docker or Gradio Space or a separate backend, instead. + +At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path. For serverless GPU platforms, see [gpu-clouds.md](gpu-clouds.md). + +## 6. Verify + +REASONED: HTTPS redirect, application-authentication and deployed-secret checks; no PaaS application, tenant credentials or deployed client bundle was supplied for this metadata review, and no run is recorded. The expected readings below follow the cited platform documentation and the guide's application-security reasoning; exact application behaviour remains deployment-specific. + +```bash +# 1. This guide checks HTTP->HTTPS redirection. Spot-check on / only: it does not prove HTTPS on every route/method, nor that +# the app's proxy-awareness (section 3) is set for secure cookies. --noproxy so a client proxy cannot answer. +# Documented: Vercel 308; Fly HTTP handlers with force_https enabled 301. Render's code is unstated. +# Railway documents GET 301 and POST-to-GET, not this HEAD response; Heroku's redirect policy is unestablished. +# This guide's check: require 301/302/307/308 with an https:// Location; HTTP content without that redirect fails. +curl -q -g -sI --noproxy '*' -w 'http=%{http_code}\n' http://app.example.com/ # this guide's redirect check above +# 2. Your APP's own auth must reject an unauthenticated request, NOT the platform deployment-protection gate. +# Vercel documents that Deployment Protection requires authentication for all requests, and that a valid +# Protection Bypass for Automation token lets the request access the deployment without authentication, +# sent as the x-vercel-protection-bypass HTTP header or query parameter carrying the project's generated +# secret. Disable deployment protection (or use that bypass) so only application auth is in play. That +# bypass is Vercel's; no Render or Fly equivalent is cited here, so do not assume one exists. If you use +# the bypass secret, send it as the header, supplied from a file with -H @file per this guide's +# conventions so it stays out of shell history; the query-parameter form writes it into history and logs. +# The status codes below are this guide's expected reading; the cited pages state none, including for the +# platform gate's own challenge (its SSO 401 below): +curl -q -g -s --noproxy '*' -o /dev/null -w 'http=%{http_code}\n' https://app.example.com/REPLACE_WITH_PROTECTED_PATH # no creds: want the APP's own 401/403, not a platform SSO 401 +# Then repeat with a valid application credential (your app's session cookie or API token, supplied from a file with +# -b or -H @file so it stays out of shell history) and require a 2xx: that positive control proves the endpoint is +# reachable and that the app's own auth, not the platform gate, is what gates it. +# 3. Secrets: run secrets.md's history and working-tree scans AND grep the DEPLOYED client bundle (JS, source maps, +# generated config, runtime-injected values) for every credential type, not just private keys. No matches is evidence, not proof. +# Why the deployed bundle: as the Next.js case, the pinned v16.4.2 docs say NEXT_PUBLIC_ values are inlined into +# the JavaScript bundle delivered to the client at build time, and production browser source maps are disabled +# unless productionBrowserSourceMaps opts in. What to grep and which credential types are this guide's procedure, +# delegated to secrets.md, not taken from the cited pages. +``` + +## Sources (checked September 2026) + +- Render: https://render.com/docs ; Fly.io: https://fly.io/docs ; Vercel: https://vercel.com/docs (each documents managed TLS and environment configuration; consult your platform's pages for the exact toggles) +- Vercel Deployment Protection: https://vercel.com/docs/deployment-protection +- Vercel changelog, protect production deployments for free on every plan (9 September 2026): https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan +- Vercel deployment protection bypass methods (checked October 2026): https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection +- Vercel Protection Bypass for Automation header and query parameter (checked October 2026): https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation +- Hugging Face Spaces: https://huggingface.co/docs/hub/spaces-overview and https://huggingface.co/docs/hub/spaces-config-reference +- Hugging Face Hub Space visibility choices and source/app access (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45 +- Hugging Face Hub Space duplication visibility default (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118 +- Hugging Face Hub organization visibility controls and plan scope (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27 +- Hugging Face Hub Dev Mode plan requirement and servers (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14 +- Hugging Face Hub Dev Mode SSH, editor connection and SSH key requirement (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78 +- Render managed PostgreSQL (external access is open by default and can be restricted; internal connections do not support `sslmode=verify-ca`/`verify-full`): https://render.com/docs/postgresql-creating-connecting +- Next.js 15 environment variables (checked October 2026; `NEXT_PUBLIC_` values in browser JavaScript): https://nextjs.org/docs/15/app/guides/environment-variables +- Next.js v16.4.2 `NEXT_PUBLIC_` build-time inlining into client JavaScript (checked October 2026): https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164 +- Next.js v16.4.2 production browser source maps default and opt-in serving (checked October 2026): https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18 +- Express 5.x request protocol (checked October 2026; `trust proxy` and `X-Forwarded-Proto`): https://expressjs.com/en/5x/api/request/#req.protocol +- Django 6.1 settings (checked October 2026; `SECURE_PROXY_SSL_HEADER` and trusted proxy requirements): https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header +- MySQL 8.4 client connection options (checked October 2026; `--ssl-mode=VERIFY_IDENTITY` certificate and hostname verification): https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode +- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services +- Render managed TLS (checked October 2026): https://render.com/docs/tls +- Render custom-domain prerequisites (checked October 2026): https://render.com/docs/custom-domains +- Vercel TLS termination (checked October 2026): https://vercel.com/docs/cdn-security/encryption +- Vercel certificate validation and renewal (checked October 2026): https://vercel.com/docs/domains/working-with-ssl +- Vercel Functions runtimes (checked October 2026): https://vercel.com/docs/functions/runtimes +- Heroku certificate automation and limitations (checked October 2026): https://devcenter.heroku.com/articles/automated-certificate-management +- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing +- Heroku custom-domain prerequisites (checked October 2026): https://devcenter.heroku.com/articles/custom-domains +- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles +- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations +- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 +- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 +- Fly domain verification for issuance and renewal (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44 +- Fly wildcard validation and automatic issuance (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87 +- Fly imported certificates and fallback (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102 +- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 +- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 +- Railway custom-domain DNS and plan conditions (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122 +- Railway certificate renewal and Cloudflare limitations (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247 +- Railway root-domain DNS (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150 +- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 +- Fly app configuration file (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7 +- Fly HTTP service redirect setting (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350 +- Fly redirect status and HTTP-handler restriction (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229 +- Fly HTTP handler forwarded-protocol header (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123 +- Railway public-networking headers and request handling (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17 +- Railway GET redirect status and POST conversion (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38 + + +## 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/paas.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): - render: Render documentation (unknown) - fly: Fly.io documentation (unknown) +- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) - vercel: Vercel documentation (unknown) - spaces: Hugging Face Spaces documentation (unknown) -- nextjs: Next.js environment variables (15) -- express: Express request protocol (5.x) -- django: Django proxy security setting (6.1) -- mysql-client: MySQL client connection options (8.4) - spaces-pinned: Hugging Face Hub documentation (6fb324369143cb76a723c88a592010514ac1095a) - heroku: Heroku documentation (unknown) -- fly-pinned: Fly.io pinned documentation (2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9) - railway: Railway documentation (72354493198302677fb6be6c692dd75c64cae2c6) - nextjs-pinned: Next.js pinned documentation (v16.4.2) +- nextjs: Next.js environment variables (15) +- express: Express request protocol (5.x) +- django: Django proxy security setting (6.1) +- mysql-client: MySQL client connection options (8.4) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): - managed-tls (REASONED; render, fly, vercel, heroku, fly-pinned, railway): TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate. - application-auth (REASONED; vercel): Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls. @@ -17685,130 +19006,169 @@ Claims (full source mapping in the front matter): - verify-redirect (REASONED; vercel, render, fly-pinned, railway, heroku): This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness. - verify-app-auth (REASONED; vercel): Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement. - verify-secrets (REASONED; render, fly, vercel, nextjs-pinned): Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan. - - -TLS responsibilities depend on the platform and deployment mode: - -- Render: the load balancer terminates HTTPS for web services and forwards HTTP to the app. Render automatically issues and renews certificates for added custom domains, including wildcards. Add the domain, configure DNS, and verify it in the dashboard. Wildcards require the root domain to point to Render and the prescribed wildcard, `_acme-challenge`, and `_cf-custom-hostname` CNAME records; existing CAA records must allow Let's Encrypt and Google Trust Services, with `issuewild` records for wildcards. At the time of writing, included custom-domain quotas depend on the workspace plan and additional domains cost extra. -- Fly.io: Fly Proxy's `tls` handler terminates TLS and forwards plaintext; without handlers, TCP passes through and the app can terminate TLS itself. Add the custom domain and configure DNS for automatic certificate issuance. Issuance and renewal require domain verification through an AAAA record pointing to the app, an `_acme-challenge` CNAME, or a `_fly-ownership` TXT record; wildcard certificates require DNS-01 with the `_acme-challenge` CNAME. These passages describe managed certificates and renewal prerequisites but do not explicitly establish automatic renewal. Imported certificates require ownership verification and take priority over a coexisting Fly-managed fallback. -- Railway: correctly configured custom domains receive automatically issued Let's Encrypt certificates, renewed with 30 days of their 90-day validity remaining; external certificates are unsupported. Both the supplied CNAME and ownership TXT records are required; wildcards also require the `_acme-challenge` CNAME, unproxied on Cloudflare. Root domains require CNAME flattening or dynamic ALIAS records. At the time of writing, custom-domain limits are one on Trial, two per service on Hobby and twenty per service by default on Pro, with increases available on request; Freenom domains are unsupported. For Cloudflare-proxied domains, issuance can fail and Railway can instead serve its default `*.up.railway.app` certificate; the docs require Full mode and warn that Full (Strict) will not work as intended. Cloudflare proxying below first-level subdomains requires Advanced Certificate Manager or switching those records to DNS Only. The cited passages establish Railway's HTTPS certificate handling, but not the precise TLS termination component. -- Vercel: its CDN terminates TLS. Certificates for deployment URLs are generated and kept updated automatically. Custom-domain issuance requires successful validation after DNS propagation; non-wildcards use HTTP-01 reachable at Vercel, and wildcards require Vercel nameservers for DNS-01. Vercel automatically attempts renewal 14 to 30 days before expiry; correct DNS, proxy, and CAA configuration must still permit validation. At the time of writing, only Enterprise teams can configure custom SSL; uploaded certificates cannot be renewed automatically by Vercel. The `/.well-known` path is reserved and cannot be redirected or rewritten. -- Heroku: the HTTP routing page describes load-balancer SSL termination, with separate Private Spaces routers. At the time of writing, ACM is included free for all applications; enable it for automatic issuance and renewal one month before expiry, using the correct Heroku DNS target. Adding custom domains requires a verified Heroku account. Cedar Private Spaces require a running web dyno for validation and renewal, cannot use ACM with Trusted IPs blocking access, and cannot use ACM for wildcard domains. The page says customers must upload their own wildcard certificate for Private Space apps, but its limitations also say "For Generation Private Space apps" can use ACM with wildcards; Fir wildcard eligibility is therefore not established consistently here. Uploading a certificate disables ACM. CAA must permit Let's Encrypt, and DNS/CDN configuration must allow validation; ACM must be disabled if the app requires the platform-handled ACME challenge paths. - -This guide's advice: where the platform terminates TLS and manages the required certificate, do not bolt certbot or an extra TLS reverse proxy onto the app, and do not apply this repository's server-TLS guides to that managed ingress. What stays yours: - -## 1. Authentication: entirely yours - -The platform does not authenticate your application's users. Some platforms protect the deployment itself: Vercel Deployment Protection, for example, can require a Vercel login to open preview and deployment URLs on every plan, and production domains too once the All Deployments scope is selected, which Vercel's changelog of 9 September 2026 made free on every plan including Hobby; Password Protection is not available on Hobby, is a paid add-on per protected project on Pro, and is included on Enterprise (at the time of writing). That gate keeps the public out of a preview; it is not your app's login. Every non-public endpoint still needs login or keys per [authentication.md](authentication.md), MFA where viable per [mfa.md](mfa.md), and a hosted provider makes both easy ([identity-providers.md](identity-providers.md)). "It is on Vercel" changes nothing about an open `/api/admin`. - -## 2. Secrets: use the platform's store -Each platform provides environment/secret configuration. Set secrets there; never commit `.env` files ([secrets.md](secrets.md)). Rotate anything that ever appeared in the repository, build logs, or client bundles. Public frontend frameworks compile some env vars into the client (for example `NEXT_PUBLIC_`-prefixed values); only put genuinely public values in those. - -## 3. Enforce HTTPS and correct proxy awareness - -- Redirect HTTP to HTTPS. This guide's advice: use the documented automatic redirect, enable the documented platform setting, or implement the redirect in the app with verified proxy trust: - - Render's TLS page says all HTTP requests are automatically redirected to HTTPS; it does not state a status code. - - Fly.io's pinned configuration reference documents `force_https = true` under `[http_service]` in `fly.toml` for apps managed by Fly Launch. The pinned services page says enabling `force_https` returns 301 for all HTTP requests; the option is only valid on HTTP handlers, and deployment fails if it is set on other handlers. - - Railway's pinned public-networking specifications describe automatic HTTPS redirects for plain HTTP GET requests with 301, and HTTP POST redirects that convert the request to GET. They do not establish a configurable redirect toggle, a POST status code, or the HEAD response used below. - - Vercel's encryption page says its CDN automatically redirects HTTP requests to HTTPS with 308 and that this redirection cannot be disabled. - - Heroku's cited HTTP routing page does not establish automatic HTTPS redirection, a redirect toggle, an application-only redirect contract, or a status code. This guide's advice: implement application-side redirection unless the deployed ingress is known to enforce it, and verify the result. - - For application-side handling, this guide's advice is to check the platform's forwarded-protocol header only after verifying the framework's proxy trust requirements. Fly's HTTP handler documents `X-Forwarded-Proto` as the original client protocol, `http` or `https`; Railway's public-networking specifications say it always indicates `https`. Heroku's routing page describes it as the originating protocol but explicitly warns that forwarded headers, including `X-Forwarded-Proto`, are not trusted because the order of pre-existing fields is unknown. These descriptions do not establish header sanitization; the cited Render and Vercel TLS pages do not establish a forwarded-protocol header contract. Keep the Express 5.x `trust proxy` and Django 6.1 `SECURE_PROXY_SSL_HEADER` trust requirements in view when implementing this guide's advice. -- Behind the platform proxy, configure the framework accordingly (`trust proxy` in Express per [nodejs.md](nodejs.md), `SECURE_PROXY_SSL_HEADER` in Django per [python.md](python.md)) so secure cookies and redirects behave. -- Bind the listener required by the deployment model. Render web services must bind `0.0.0.0`; Render recommends `PORT` (default port 10000, configurable), can usually detect another bound port, and requires the public HTTP server to use `PORT` when multiple ports are bound. Fly Proxy HTTP services must be reachable over IPv4: use `0.0.0.0` and match the service's configured `internal_port` to the app listener; IPv4-mapped IPv6 wildcard behaviour depends on the VM and runtime. Railway domains route to a detected or selected internal target port; the cited pages do not establish an injected `PORT` or required listener address. Heroku assigns web processes a `PORT` environment variable; for Fir, the Generations page says Private Spaces networking uses IPv6 by default, that IPv4-only assumptions do not bind properly, and recommends an IPv6 or dual-stack listener (typically `::`); the cited pages do not establish Cedar's listener address. Vercel runtimes transform code into Functions served by its CDN, including Node.js function entrypoints and Python ASGI/WSGI apps; that page establishes neither a universal injected port nor a required listener address. This guide's advice: bind nothing else and do not open extra listeners. - -## 4. Databases attached to PaaS apps - -Managed databases from these platforms come with TLS endpoints, but check what the specific endpoint supports: require certificate and hostname verification with the client's own setting (`sslmode=verify-full` for PostgreSQL, `--ssl-mode=VERIFY_IDENTITY` for MySQL) per the database guides ([postgresql.md](postgresql.md), [mysql.md](mysql.md)) where the endpoint allows it, and note that some managed internal endpoints support encryption but not `verify-full` (Render's internal PostgreSQL connections, where TLS is optional, do not support `verify-ca`/`verify-full`); there, set `sslmode=require` to force encryption and prevent a plaintext fallback, and do not treat that encryption-only connection as identity-verified. Keep the credentials in the platform's secret store, and restrict the network too: many managed databases are reachable from any IP by default (Render's Postgres is), so prefer private connectivity and disable or source-restrict external access with the provider's database network controls, not TLS alone. Databases you run yourself elsewhere follow their own guides plus [cloud-firewalls.md](cloud-firewalls.md). - -## 5. Hugging Face Spaces - -This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only. Visibility can be changed to "protected" (the source stays private to the owner and collaborators, but the running app is still public through its embed URL or custom domain) or "private" (both the source and the running app are restricted to the owner and collaborators); protected visibility requires a paid plan, PRO for personal accounts or Team/Enterprise for organizations, at the time of writing. Space secrets belong in the Settings tab's secrets store, never in the repository or its README metadata; anything set as a "variable" instead of a "secret" is still publicly readable. That secrets store is not a safe place for backend or provider credentials on a Static Space: Hugging Face exposes both variables and secrets to client-side JavaScript there, through `window.huggingface.variables`, so anything placed in a Static Space's secrets still reaches the browser. Keep server-side credentials in a server-side service, such as a Docker or Gradio Space or a separate backend, instead. - -At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path. For serverless GPU platforms, see [gpu-clouds.md](gpu-clouds.md). - -## 6. Verify - -REASONED: HTTPS redirect, application-authentication and deployed-secret checks; no PaaS application, tenant credentials or deployed client bundle was supplied for this metadata review, and no run is recorded. The expected readings below follow the cited platform documentation and the guide's application-security reasoning; exact application behaviour remains deployment-specific. - -```bash -# 1. This guide checks HTTP->HTTPS redirection. Spot-check on / only: it does not prove HTTPS on every route/method, nor that -# the app's proxy-awareness (section 3) is set for secure cookies. --noproxy so a client proxy cannot answer. -# Documented: Vercel 308; Fly HTTP handlers with force_https enabled 301. Render's code is unstated. -# Railway documents GET 301 and POST-to-GET, not this HEAD response; Heroku's redirect policy is unestablished. -# This guide's check: require 301/302/307/308 with an https:// Location; HTTP content without that redirect fails. -curl -q -g -sI --noproxy '*' -w 'http=%{http_code}\n' http://app.example.com/ # this guide's redirect check above -# 2. Your APP's own auth must reject an unauthenticated request, NOT the platform deployment-protection gate. -# Vercel documents that Deployment Protection requires authentication for all requests, and that a valid -# Protection Bypass for Automation token lets the request access the deployment without authentication, -# sent as the x-vercel-protection-bypass HTTP header or query parameter carrying the project's generated -# secret. Disable deployment protection (or use that bypass) so only application auth is in play. That -# bypass is Vercel's; no Render or Fly equivalent is cited here, so do not assume one exists. If you use -# the bypass secret, send it as the header, supplied from a file with -H @file per this guide's -# conventions so it stays out of shell history; the query-parameter form writes it into history and logs. -# The status codes below are this guide's expected reading; the cited pages state none, including for the -# platform gate's own challenge (its SSO 401 below): -curl -q -g -s --noproxy '*' -o /dev/null -w 'http=%{http_code}\n' https://app.example.com/REPLACE_WITH_PROTECTED_PATH # no creds: want the APP's own 401/403, not a platform SSO 401 -# Then repeat with a valid application credential (your app's session cookie or API token, supplied from a file with -# -b or -H @file so it stays out of shell history) and require a 2xx: that positive control proves the endpoint is -# reachable and that the app's own auth, not the platform gate, is what gates it. -# 3. Secrets: run secrets.md's history and working-tree scans AND grep the DEPLOYED client bundle (JS, source maps, -# generated config, runtime-injected values) for every credential type, not just private keys. No matches is evidence, not proof. -# Why the deployed bundle: as the Next.js case, the pinned v16.4.2 docs say NEXT_PUBLIC_ values are inlined into -# the JavaScript bundle delivered to the client at build time, and production browser source maps are disabled -# unless productionBrowserSourceMaps opts in. What to grep and which credential types are this guide's procedure, -# delegated to secrets.md, not taken from the cited pages. -``` - -## Sources (checked September 2026) - -- Render: https://render.com/docs ; Fly.io: https://fly.io/docs ; Vercel: https://vercel.com/docs (each documents managed TLS and environment configuration; consult your platform's pages for the exact toggles) -- Vercel Deployment Protection: https://vercel.com/docs/deployment-protection -- Vercel changelog, protect production deployments for free on every plan (9 September 2026): https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan -- Vercel deployment protection bypass methods (checked October 2026): https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection -- Vercel Protection Bypass for Automation header and query parameter (checked October 2026): https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation -- Hugging Face Spaces: https://huggingface.co/docs/hub/spaces-overview and https://huggingface.co/docs/hub/spaces-config-reference -- Hugging Face Hub Space visibility choices and source/app access (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45 -- Hugging Face Hub Space duplication visibility default (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118 -- Hugging Face Hub organization visibility controls and plan scope (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27 -- Hugging Face Hub Dev Mode plan requirement and servers (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14 -- Hugging Face Hub Dev Mode SSH, editor connection and SSH key requirement (checked October 2026; commit 6fb324369143cb76a723c88a592010514ac1095a): https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78 -- Render managed PostgreSQL (external access is open by default and can be restricted; internal connections do not support `sslmode=verify-ca`/`verify-full`): https://render.com/docs/postgresql-creating-connecting -- Next.js 15 environment variables (checked October 2026; `NEXT_PUBLIC_` values in browser JavaScript): https://nextjs.org/docs/15/app/guides/environment-variables -- Next.js v16.4.2 `NEXT_PUBLIC_` build-time inlining into client JavaScript (checked October 2026): https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164 -- Next.js v16.4.2 production browser source maps default and opt-in serving (checked October 2026): https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18 -- Express 5.x request protocol (checked October 2026; `trust proxy` and `X-Forwarded-Proto`): https://expressjs.com/en/5x/api/request/#req.protocol -- Django 6.1 settings (checked October 2026; `SECURE_PROXY_SSL_HEADER` and trusted proxy requirements): https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header -- MySQL 8.4 client connection options (checked October 2026; `--ssl-mode=VERIFY_IDENTITY` certificate and hostname verification): https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode -- Render web-service listener and TLS termination (checked October 2026): https://render.com/docs/web-services -- Render managed TLS (checked October 2026): https://render.com/docs/tls -- Render custom-domain prerequisites (checked October 2026): https://render.com/docs/custom-domains -- Vercel TLS termination (checked October 2026): https://vercel.com/docs/cdn-security/encryption -- Vercel certificate validation and renewal (checked October 2026): https://vercel.com/docs/domains/working-with-ssl -- Vercel Functions runtimes (checked October 2026): https://vercel.com/docs/functions/runtimes -- Heroku certificate automation and limitations (checked October 2026): https://devcenter.heroku.com/articles/automated-certificate-management -- Heroku HTTP routing (checked October 2026): https://devcenter.heroku.com/articles/http-routing -- Heroku custom-domain prerequisites (checked October 2026): https://devcenter.heroku.com/articles/custom-domains -- Heroku web-process port assignment (checked October 2026): https://devcenter.heroku.com/articles/runtime-principles -- Heroku Fir Private Spaces networking and listener recommendation (checked October 2026): https://devcenter.heroku.com/articles/generations -- Fly TCP passthrough (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97 -- Fly TLS termination (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176 -- Fly domain verification for issuance and renewal (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44 -- Fly wildcard validation and automatic issuance (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87 -- Fly imported certificates and fallback (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102 -- Fly public service binding (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42 -- Fly IPv4 and IPv6 wildcard qualification (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92 -- Railway custom-domain DNS and plan conditions (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122 -- Railway certificate renewal and Cloudflare limitations (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247 -- Railway root-domain DNS (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150 -- Railway target ports (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146 -- Fly app configuration file (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7 -- Fly HTTP service redirect setting (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350 -- Fly redirect status and HTTP-handler restriction (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229 -- Fly HTTP handler forwarded-protocol header (checked October 2026; commit 2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9): https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123 -- Railway public-networking headers and request handling (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17 -- Railway GET redirect status and POST conversion (checked October 2026; commit 72354493198302677fb6be6c692dd75c64cae2c6): https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38 +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "2db22068c994d124ee04bbb96560a48925d4f295f118e343bc0e18ff0d9155b5", + "components": { + "render": { + "name": "Render documentation", + "basis": "unknown", + "sources": { + "s213783ba6209": "https://render.com/docs", + "se240afec8d2d": "https://render.com/docs/postgresql-creating-connecting", + "s1820ac471b6c": "https://render.com/docs/web-services", + "s5b8400c2a2ba": "https://render.com/docs/tls", + "s22963948fc90": "https://render.com/docs/custom-domains" + } + }, + "fly": { + "name": "Fly.io documentation", + "basis": "unknown", + "sources": { + "s1b202c527f2e": "https://fly.io/docs" + } + }, + "fly-pinned": { + "name": "Fly.io pinned documentation", + "basis": "2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9", + "sources": { + "s254c8c8a5edd": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L93-L97", + "s32999aa4c58c": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L174-L176", + "sf4105f79b22b": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L39-L44", + "sea5560c14d13": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L64-L87", + "sb935e8478a30": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/custom-domain.mdx#L91-L102", + "s62fec91543a1": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L33-L42", + "s61c1e50d2c11": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/app-services.mdx#L92-L92", + "s6e9a7292860d": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L7-L7", + "sa63b828c93df": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/reference/configuration.mdx#L329-L350", + "s4324a02dbc42": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L229-L229", + "scf2e6f3bce02": "https://github.com/superfly/docs/blob/2f68a40e09bb6ef4c4ac6f90c15c1cf5a176d7e9/networking/services.mdx#L117-L123" + } + }, + "vercel": { + "name": "Vercel documentation", + "basis": "unknown", + "sources": { + "s2b470a6bc853": "https://vercel.com/docs", + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection", + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan", + "s3e7832fd3031": "https://vercel.com/docs/cdn-security/encryption", + "se314aa4b7f1e": "https://vercel.com/docs/domains/working-with-ssl", + "s22498401f5d0": "https://vercel.com/docs/functions/runtimes", + "s845513031876": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection", + "sc1588029e14e": "https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation" + } + }, + "spaces": { + "name": "Hugging Face Spaces documentation", + "basis": "unknown", + "sources": { + "sda0c810c053d": "https://huggingface.co/docs/hub/spaces-overview", + "sd5ec2b2772e0": "https://huggingface.co/docs/hub/spaces-config-reference" + } + }, + "spaces-pinned": { + "name": "Hugging Face Hub documentation", + "basis": "6fb324369143cb76a723c88a592010514ac1095a", + "sources": { + "se2fb4e2a5c02": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L29-L45", + "s9d332f0940e2": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-overview.md#L114-L118", + "s969a28a4c44e": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/enterprise-advanced-security.md#L3-L27", + "s5d393b019bad": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L3-L14", + "sd1ae2b703a70": "https://github.com/huggingface/hub-docs/blob/6fb324369143cb76a723c88a592010514ac1095a/docs/hub/spaces-dev-mode.md#L41-L78" + } + }, + "heroku": { + "name": "Heroku documentation", + "basis": "unknown", + "sources": { + "sa86eb06bbd4a": "https://devcenter.heroku.com/articles/automated-certificate-management", + "s0c5bf300cef4": "https://devcenter.heroku.com/articles/http-routing", + "sfbcf716168be": "https://devcenter.heroku.com/articles/custom-domains", + "s370d4beec29a": "https://devcenter.heroku.com/articles/runtime-principles", + "s211f5fe76042": "https://devcenter.heroku.com/articles/generations" + } + }, + "railway": { + "name": "Railway documentation", + "basis": "72354493198302677fb6be6c692dd75c64cae2c6", + "sources": { + "s93da6ccbfe58": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L39-L122", + "s6d4dc8fb7784": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L220-L247", + "s2bc94f13536b": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L150-L150", + "sac1383056b65": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/domains/working-with-domains.md#L126-L146", + "sbf08758c106a": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L16-L17", + "s844d338bd121": "https://github.com/railwayapp/docs/blob/72354493198302677fb6be6c692dd75c64cae2c6/content/docs/networking/public-networking/specs-and-limits.md#L37-L38" + } + }, + "nextjs-pinned": { + "name": "Next.js pinned documentation", + "basis": "v16.4.2", + "sources": { + "s0e3d2cd2248e": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/02-guides/environment-variables.mdx#L158-L164", + "sf7cfdc553311": "https://github.com/vercel/next.js/blob/v16.4.2/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" + } + }, + "nextjs": { + "name": "Next.js environment variables", + "basis": "15", + "sources": { + "s22e4b70dab8b": "https://nextjs.org/docs/15/app/guides/environment-variables" + } + }, + "express": { + "name": "Express request protocol", + "basis": "5.x", + "sources": { + "s5bc62e66824b": "https://expressjs.com/en/5x/api/request/#req.protocol" + } + }, + "django": { + "name": "Django proxy security setting", + "basis": "6.1", + "sources": { + "sb5e26f65fd91": "https://docs.djangoproject.com/en/6.1/ref/settings/#secure-proxy-ssl-header" + } + }, + "mysql-client": { + "name": "MySQL client connection options", + "basis": "8.4", + "sources": { + "s823c0bfaaafa": "https://dev.mysql.com/doc/refman/8.4/en/connection-options.html#option_general_ssl-mode" + } + } + }, + "claims": { + "managed-tls": {"text": "TLS termination and certificate handling depend on platform and deployment conditions; Fly.io passes TCP through without handlers, and cited pages do not establish Fly automatic renewal or Railway's termination component; Heroku Fir wildcard eligibility remains unclear. This guide's advice: no certbot or extra TLS proxy where the platform terminates TLS and manages the required certificate.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "render:s5b8400c2a2ba", "render:s22963948fc90", "vercel:s3e7832fd3031", "vercel:se314aa4b7f1e", "heroku:sa86eb06bbd4a", "heroku:s0c5bf300cef4", "heroku:sfbcf716168be", "fly-pinned:s254c8c8a5edd", "fly-pinned:s32999aa4c58c", "fly-pinned:sf4105f79b22b", "fly-pinned:sea5560c14d13", "fly-pinned:sb935e8478a30", "railway:s93da6ccbfe58", "railway:s6d4dc8fb7784", "railway:s2bc94f13536b"], "status": "REASONED"}, + "application-auth": {"text": "Deployment protection does not replace app login, endpoint keys or application MFA; non-public endpoints still need their own access controls.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "vercel-login": {"text": "Vercel login can gate preview/deployment URLs on every plan and production domains with All Deployments; the 9 September 2026 change made that scope free including Hobby.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s7836e043434a"], "status": "REASONED"}, + "vercel-password": {"text": "At the recorded documentation date, Password Protection is unavailable on Hobby, a paid per-project add-on on Pro and included on Enterprise.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "secret-store": {"text": "Use platform environment/secret configuration, not committed .env files; rotate credentials exposed in repositories, build logs or client bundles.", "components": ["render", "fly", "vercel"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853"], "status": "REASONED"}, + "public-env": {"text": "Only public values belong in browser-compiled environment variables such as NEXT_PUBLIC_.", "components": ["vercel", "nextjs"], "sources": ["vercel:s2b470a6bc853", "nextjs:s22e4b70dab8b"], "status": "REASONED"}, + "https-redirect": {"text": "Enforce HTTPS: Render redirects automatically (code unstated), Vercel uses mandatory 308, Fly HTTP handlers use 301 with force_https, and Railway documents GET 301 and POST-to-GET, not HEAD. Heroku's redirect policy is unestablished and its forwarded headers are untrusted; this guide advises app handling with verified proxy trust, without assuming header sanitization.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s5b8400c2a2ba", "vercel:s3e7832fd3031", "heroku:s0c5bf300cef4", "express:s5bc62e66824b", "django:sb5e26f65fd91", "fly-pinned:s6e9a7292860d", "fly-pinned:sa63b828c93df", "fly-pinned:s4324a02dbc42", "fly-pinned:scf2e6f3bce02", "railway:sbf08758c106a", "railway:s844d338bd121"], "status": "REASONED"}, + "proxy-awareness": {"text": "Configure Express trust proxy or Django SECURE_PROXY_SSL_HEADER so secure cookies and redirects work behind the platform.", "components": ["render", "fly", "vercel", "express", "django"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "express:s5bc62e66824b", "django:sb5e26f65fd91"], "status": "REASONED"}, + "listener-port": {"text": "Listener requirements depend on deployment; Heroku recommends IPv6 or dual-stack for Fir Private Spaces. Cited pages do not establish Railway's or Vercel Functions' listener/port contracts or Heroku Cedar's listener address; this guide's advice: bind nothing else and do not open extra listeners.", "components": ["render", "fly", "vercel", "heroku", "fly-pinned", "railway"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "render:s1820ac471b6c", "vercel:s22498401f5d0", "heroku:s370d4beec29a", "heroku:s211f5fe76042", "fly-pinned:s62fec91543a1", "fly-pinned:s61c1e50d2c11", "railway:sac1383056b65"], "status": "REASONED"}, + "database-identity": {"text": "Where supported, require TLS certificate/hostname verification with PostgreSQL sslmode=verify-full or MySQL --ssl-mode=VERIFY_IDENTITY.", "components": ["render", "mysql-client"], "sources": ["render:se240afec8d2d", "mysql-client:s823c0bfaaafa"], "status": "REASONED"}, + "render-internal-tls": {"text": "Render internal PostgreSQL TLS is optional and supports neither verify-ca nor verify-full; sslmode=require prevents plaintext fallback but is not identity verification.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, + "database-network": {"text": "Render Postgres external access is open to any IP by default; prefer private connectivity and disable or source-restrict external access. Keep database credentials in the secret store.", "components": ["render"], "sources": ["render:se240afec8d2d"], "status": "REASONED"}, + "spaces-creation": {"text": "This guide's advice: explicitly select and verify visibility when you create a Space. The pinned docs describe public, protected, and private visibility; public visibility exposes both the source and the running app, and duplicating a Space defaults to private. At the time of writing, the pinned Advanced Security docs attach repository visibility controls to Team & Enterprise organizations: Public by default creates new repositories with public visibility; Private by default creates new repositories with private visibility, and changing that setting does not affect existing repositories; Private only enforces private visibility for all new repositories, with only organization admins able to change visibility settings. Those lines do not establish a general creation default or a retroactive conversion of existing repositories by Private only.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:se2fb4e2a5c02", "spaces-pinned:s9d332f0940e2", "spaces-pinned:s969a28a4c44e"], "status": "REASONED"}, + "spaces-public": {"text": "Public Spaces expose both source and running app.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-protected": {"text": "Protected Spaces keep source private to owner/collaborators but expose the app via embed URL or custom domain; the recorded requirement is PRO for personal accounts or Team/Enterprise for organizations.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-private": {"text": "Private Spaces restrict both source and running app to the owner and collaborators.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-secrets": {"text": "Use Settings secrets for sensitive values, never repository/README metadata; variables remain publicly readable.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d", "spaces:sd5ec2b2772e0"], "status": "REASONED"}, + "spaces-static": {"text": "Static Spaces expose both variables and secrets to client JavaScript through window.huggingface.variables; keep backend/provider credentials in Docker/Gradio Spaces or another server-side service.", "components": ["spaces"], "sources": ["spaces:sda0c810c053d"], "status": "REASONED"}, + "spaces-dev": {"text": "At the time of writing, the pinned Dev Mode docs require PRO or Team & Enterprise. The Dev Mode Docker image starts the application as a subprocess and starts an SSH server for local editors and terminals and a VS Code server for in-product VS Code Web. Dev Mode exposes the running Space as a standard SSH host; clients use the Space subdomain shown in the Dev Mode controls as the username at ssh.hf.space, and local VS Code uses Remote - SSH to connect to the same host. SSH requires adding the machine's SSH public key to the user's Hugging Face account; these docs do not establish which users or roles may connect, or the access control for in-product VS Code Web. This guide's assessment: access into the running container is materially more access than the app itself, so treat an unattended Dev Mode session the same as any other exposed admin path.", "components": ["spaces", "spaces-pinned"], "sources": ["spaces:sda0c810c053d", "spaces-pinned:s5d393b019bad", "spaces-pinned:sd1ae2b703a70"], "status": "REASONED"}, + "verify-redirect": {"text": "This guide's HTTP HEAD / check requires 301/302/307/308 with an HTTPS Location; Vercel documents 308 and Fly HTTP handlers 301 with force_https, but Render's code, Railway HEAD, and Heroku's redirect policy are unestablished. The spot-check proves neither every route/method nor secure-cookie proxy awareness.", "components": ["vercel", "render", "fly-pinned", "railway", "heroku"], "sources": ["vercel:s2b470a6bc853", "vercel:s3e7832fd3031", "render:s5b8400c2a2ba", "fly-pinned:s4324a02dbc42", "railway:sbf08758c106a", "railway:s844d338bd121", "heroku:s0c5bf300cef4"], "status": "REASONED", "verify": [1]}, + "verify-app-auth": {"text": "Disable Vercel deployment protection or use its documented Protection Bypass for Automation, the x-vercel-protection-bypass header or query parameter carrying the project's generated secret, to isolate app auth; no Render or Fly equivalent is cited. This guide's expected reading: anonymous protected requests get the app's 401/403, valid app credentials a 2xx, and a platform SSO 401 is not app enforcement.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s845513031876", "vercel:sc1588029e14e"], "status": "REASONED", "verify": [1]}, + "verify-secrets": {"text": "Scan history, working tree and deployed client JS, source maps, generated config and runtime values for every credential type; no matches is evidence, not proof. The scan procedure is this guide's, delegated to secrets.md; the pinned Next.js v16.4.2 lines document only the NEXT_PUBLIC_ build-time inlining and off-by-default production browser source maps that motivate the bundle scan.", "components": ["render", "fly", "vercel", "nextjs-pinned"], "sources": ["render:s213783ba6209", "fly:s1b202c527f2e", "vercel:s2b470a6bc853", "nextjs-pinned:s0e3d2cd2248e", "nextjs-pinned:sf7cfdc553311"], "status": "REASONED", "verify": [1]} + } +} +```` + ====================================================================== ==> egress-metadata.md @@ -18249,64 +19609,9 @@ Claims (full source mapping in the JSON block below): # Rented GPUs: RunPod, Vast.ai, Lambda, Modal - -**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): - -- runpod: RunPod documentation (unknown) -- vast: Vast.ai documentation (unknown) -- vast-image: Vast.ai base image (00064421641881c1f83ff58c55f13cbc10cbea4d) -- lambda: Lambda Public Cloud documentation (unknown) -- modal: Modal proxy authentication (unknown) -- jupyter: Jupyter Server security (unknown) -- jupyter-contents: Jupyter Server contents authentication (v2.18.0) -- iproute2: iproute2 ss manual (v6.12.0) -- network-namespaces: Linux network namespaces manual (man-pages-5.13) -- ollama: Ollama (v0.34.2) - -Claims (full source mapping in the front matter): +Written against RunPod documentation (no recorded version), Jupyter Server contents authentication v2.18.0, Ollama v0.34.2, iproute2 ss manual v6.12.0, and Linux network namespaces manual man-pages-5.13; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-10-19; documentation checked September 2026. The guide records 35 claims, all reasoned from cited sources. -- publication (REASONED; runpod, vast, lambda, modal): Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover. -- runpod-http (REASONED; runpod): Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication. -- runpod-tcp (REASONED; runpod): Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data. -- runpod-symmetry (REASONED; runpod): TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public. -- vast-ssh-port (REASONED; vast): Vast.ai SSH launch mode opens internal port 22 by default. -- vast-jupyter-port (REASONED; vast): Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default. -- vast-mapping (REASONED; vast): Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step. -- vast-proxy (REASONED; vast): Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation. -- vast-token (REASONED; vast): Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates. -- vast-auth-controls (REASONED; vast-image, vast): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results. -- vast-portal-routing (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential. -- vast-portal-jupyter (REASONED; vast-image): Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication. -- vast-auth-defaults (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set. -- vast-auth-routes (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD. -- vast-auth-exemptions (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there. -- vast-token-reuse (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too. -- vast-manifest-report (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to "true": reporting tests equality to "true", enforcement inequality to "false". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce. -- lambda-default (REASONED; lambda): Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch. -- lambda-rule (REASONED; lambda): An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access. -- modal-web (REASONED; modal): Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication. -- modal-dedicated (REASONED; modal): Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public. -- modal-server (REASONED; modal): Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public. -- modal-shared (REASONED; modal): Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public. -- modal-credentials (REASONED; modal): Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer <token_id>.<token_secret>; missing credentials on a protected endpoint return 401. -- runpod-bind (REASONED; runpod): RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost. -- listener-auth (REASONED; runpod, vast, jupyter, ollama): Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication. -- mfa (REASONED; modal, jupyter): MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login. -- lambda-ssh (REASONED; lambda): Lambda requires an SSH key at launch; keep the private key off the instance. -- vast-ssh (REASONED; vast): Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance. -- runpod-ssh (REASONED; runpod): RunPod recommends SSH keys and offers an optional password; use the key and skip the password. -- verify-inventory (REASONED; runpod, vast, iproute2, network-namespaces): This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside. -- verify-jupyter (REASONED; jupyter, jupyter-contents): This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls. -- verify-mapping (REASONED; runpod, vast, modal, jupyter): Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control. -- verify-portal-exempt (REASONED; vast-image): For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE. -- remote-desktop (REASONED; runpod, vast, vast-image): Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it. - +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. Unlike the major clouds, most rented-GPU platforms put no default-deny firewall in front of the box. How a bound port becomes public differs by platform: on RunPod you expose a port (HTTP or TCP), on Vast.ai a launch mode or an added port maps it to a public port, on Lambda you open a firewall rule (its inbound firewall is deny-by-default), and on Modal it depends on the serverless construct. Raw port publication and a firewall allowance add NO authentication of their own, so a listener exposed that way is reachable with only whatever auth it has itself; an authenticating platform proxy is the exception (Modal's Endpoints and Servers require proxy auth by default and Shared Endpoints always do, only a public Web endpoint does not, and Vast.ai's Instance Portal gates the routes it proxies), but do not assume one is in front of a given port. Treat every listener the same way you would on your own hardware: bind it privately or authenticate it, per [ollama.md](ollama.md) and [model-servers.md](model-servers.md). @@ -18512,73 +19817,259 @@ REASONED: following block; this guide's test design for the Vast.ai portal's pat - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop browser and direct connection choices (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50 - Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d linux-desktop documented VNC password and override (checked October 2026): https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80 -====================================================================== -==> deployment-lifecycle.md -====================================================================== - -# Deploying safely over time: verify from outside, safe order, previews, teardown +## 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/gpu-clouds.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- nmap: Nmap documentation (unknown) -- shodan: Shodan (unknown) -- censys: Censys (unknown) -- vercel: Vercel Deployment Protection (unknown) -- access: Cloudflare Access policies (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- dns: BIND dig documentation (9.18.39) -- openssl: OpenSSL command documentation (3.0) -- openbsd-nc: OpenBSD nc manual (7.6) -- owasp-takeover: OWASP subdomain takeover prevention (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) -- wstg: OWASP Web Security Testing Guide (v4.2) -- dns-rfcs: DNS caching RFCs (unknown) -- owasp-docs: OWASP security guidance (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) -- entra-docs: Microsoft Entra revocation model (231747abc59ae3d50c74e215c1cdd592eb980723) -- cloudflare-access: Cloudflare Access and origin configuration (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- oauth2-proxy: OAuth2 Proxy (v7.15.4) -- authelia: Authelia (v4.39.28) -- oauth-revocation: OAuth 2.0 Token Revocation RFC (unknown) -- nist: NIST digital identity guidelines (SP 800-63B-4) -- ubuntu-nc: Ubuntu noble netcat-openbsd source (c047f1a4572e796c8643fb7e5a719a9d960a677b) -- c9s-nmap: CentOS Stream 9 nmap packaging (17c2b7795d821cd7fbe311cf3f885be3ea1f122f) -- busybox: BusyBox 1.36.1 (1_36_1) -- docker-notes: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) -- webui: Open WebUI (0a7c15832fb30b1903753e83f81dc7d27e5b0944) -- certbot-v230: Certbot user guide and source (v2.3.0) -- le: Let's Encrypt documentation (unknown) +- runpod: RunPod documentation (unknown) +- vast: Vast.ai documentation (unknown) +- vast-image: Vast.ai base image (00064421641881c1f83ff58c55f13cbc10cbea4d) +- lambda: Lambda Public Cloud documentation (unknown) +- modal: Modal proxy authentication (unknown) +- jupyter: Jupyter Server security (unknown) +- jupyter-contents: Jupyter Server contents authentication (v2.18.0) +- ollama: Ollama (v0.34.2) +- iproute2: iproute2 ss manual (v6.12.0) +- network-namespaces: Linux network namespaces manual (man-pages-5.13) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- outside (REASONED; nmap): Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour. -- tcp-scan (REASONED; nmap, openbsd-nc, ubuntu-nc, c9s-nmap, busybox): Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive. -- udp-scan (REASONED; nmap): The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU. -- all-addresses (REASONED; nmap, vercel): Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others. -- passive-index (REASONED; shodan, censys): Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan. -- initialize-secrets (REASONED; access, webui): As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here. -- registration (REASONED; access, webui): As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here. -- fronting-order (REASONED; access, le): As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice. -- setup-route (REASONED; access, webui): After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only. -- preview-standard (REASONED; vercel): Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open. -- preview-all (REASONED; vercel): All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on. -- preview-gate (REASONED; access, vercel): Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs. -- clone-secrets (REASONED; access): As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. -- dns-retirement (REASONED; dns, owasp-takeover, wstg, dns-rfcs): Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection. -- retire-credentials (REASONED; access, owasp-docs): After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users. -- reverify (REASONED; nmap, docker-notes, webui): Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only. -- certificate-monitor (REASONED; nmap, openssl, certbot-v230): Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only. -- offboarding (REASONED; access, owasp-docs, entra-docs, cloudflare-access, oauth2-proxy, authelia, oauth-revocation, nist): When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations. -- verify-ipv4 (REASONED; nmap): From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive. -- verify-ipv6 (REASONED; nmap): Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive. -- verify-dns (REASONED; dns, dns-rfcs): curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive. -- verify-certificate (REASONED; nmap, openssl): The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only. +- publication (REASONED; runpod, vast, lambda, modal): Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover. +- runpod-http (REASONED; runpod): Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication. +- runpod-tcp (REASONED; runpod): Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data. +- runpod-symmetry (REASONED; runpod): TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public. +- vast-ssh-port (REASONED; vast): Vast.ai SSH launch mode opens internal port 22 by default. +- vast-jupyter-port (REASONED; vast): Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default. +- vast-mapping (REASONED; vast): Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step. +- vast-proxy (REASONED; vast): Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation. +- vast-token (REASONED; vast): Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates. +- vast-auth-controls (REASONED; vast-image, vast): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results. +- vast-portal-routing (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential. +- vast-portal-jupyter (REASONED; vast-image): Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication. +- vast-auth-defaults (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set. +- vast-auth-routes (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD. +- vast-auth-exemptions (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there. +- vast-token-reuse (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too. +- vast-manifest-report (REASONED; vast-image): At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to "true": reporting tests equality to "true", enforcement inequality to "false". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce. +- lambda-default (REASONED; lambda): Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch. +- lambda-rule (REASONED; lambda): An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access. +- modal-web (REASONED; modal): Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication. +- modal-dedicated (REASONED; modal): Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public. +- modal-server (REASONED; modal): Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public. +- modal-shared (REASONED; modal): Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public. +- modal-credentials (REASONED; modal): Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer <token_id>.<token_secret>; missing credentials on a protected endpoint return 401. +- runpod-bind (REASONED; runpod): RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost. +- listener-auth (REASONED; runpod, vast, jupyter, ollama): Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication. +- mfa (REASONED; modal, jupyter): MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login. +- lambda-ssh (REASONED; lambda): Lambda requires an SSH key at launch; keep the private key off the instance. +- vast-ssh (REASONED; vast): Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance. +- runpod-ssh (REASONED; runpod): RunPod recommends SSH keys and offers an optional password; use the key and skip the password. +- verify-inventory (REASONED; runpod, vast, iproute2, network-namespaces): This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside. +- verify-jupyter (REASONED; jupyter, jupyter-contents): This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls. +- verify-mapping (REASONED; runpod, vast, modal, jupyter): Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control. +- verify-portal-exempt (REASONED; vast-image): For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE. +- remote-desktop (REASONED; runpod, vast, vast-image): Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it. + +````json +{ + "schema": 1, + "checked": "2026-10-19", + "documentation_checked": "2026-09", + "body_sha256": "7dc59d278092451cbf9bc33836b92a411a629f5285413f75b1409ecbf11a9774", + "components": { + "runpod": { + "name": "RunPod documentation", + "basis": "unknown", + "sources": { + "s817198f039ee": "https://docs.runpod.io/pods/configuration/expose-ports", + "s13c43b856d4f": "https://docs.runpod.io/pods/configuration/use-ssh" + } + }, + "vast": { + "name": "Vast.ai documentation", + "basis": "unknown", + "sources": { + "sc25989a1613e": "https://docs.vast.ai/guides/instances/connect/networking", + "s0bdbda534723": "https://docs.vast.ai/guides/instances/connect/instance-portal", + "s6e0303ef19fe": "https://docs.vast.ai/guides/instances/connect/ssh" + } + }, + "vast-image": { + "name": "Vast.ai base image", + "basis": "00064421641881c1f83ff58c55f13cbc10cbea4d", + "sources": { + "sf75c3c07819e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md", + "s5d2b91002bac": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L175-L187", + "s229c9334547e": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L135-L147", + "s3b735c4b3632": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/README.md#L154-L158", + "s037a7cbd778c": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/instance-tools/tests/base/26-caddy-auth.sh#L97-L110", + "sdc669faa429d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/caddy.sh#L9-L14", + "s0428d55e9145": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/ROOT/opt/supervisor-scripts/x11vnc.sh#L22-L24", + "s1b8c6c46f358": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L47-L50", + "s44f3439f13de": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/derivatives/linux-desktop/README.template.md#L76-L80", + "sd0ba21945c9a": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L14-L40", + "sc77a7717bbc4": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L68-L71", + "s3c43b58f9e41": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L75-L93", + "sf1b910a375c6": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L125-L132", + "sae1085635ff9": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L175-L186", + "sd9c32e411f08": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L212-L240", + "s5a43e0cf8348": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L293-L308", + "sea35d951abe5": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L311-L321", + "s4a05112af859": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L323-L387", + "s5c18e60bb9e7": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/caddy_manager/caddy_config_manager.py#L400-L413", + "s92be3bd5cdef": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/launch.sh#L77-L92", + "s7ac7955e604d": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/opt/supervisor-scripts/syncthing.sh#L14-L16", + "s10acc068e127": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/ROOT/etc/vast_boot.d/10-prep-env.sh#L12-L27", + "s6d0bc9f2a0da": "https://github.com/vast-ai/base-image/blob/00064421641881c1f83ff58c55f13cbc10cbea4d/portal-aio/capabilities/manifest.py#L674-L694" + } + }, + "lambda": { + "name": "Lambda Public Cloud documentation", + "basis": "unknown", + "sources": { + "se22064748974": "https://docs.lambda.ai/public-cloud/firewalls/", + "sb12b4a953833": "https://docs.lambda.ai/public-cloud/on-demand/connecting-instance/" + } + }, + "modal": { + "name": "Modal proxy authentication", + "basis": "unknown", + "sources": { + "s7d083f819a21": "https://modal.com/docs/guide/webhook-proxy-auth", + "s12a4f5e04dbe": "https://modal.com/docs/guide/shared-endpoints", + "s33e1c2c2faf3": "https://modal.com/docs/guide/endpoints" + } + }, + "jupyter": { + "name": "Jupyter Server security", + "basis": "unknown", + "sources": { + "sc703a8030c8f": "https://jupyter-server.readthedocs.io/en/latest/operators/security.html" + } + }, + "jupyter-contents": { + "name": "Jupyter Server contents authentication", + "basis": "v2.18.0", + "sources": { + "sdc3ad9152505": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L83-L186", + "sf0a3ea8ee42e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/contents/handlers.py#L436-L445", + "s4bdc7b9183f3": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L782-L789", + "s4e7504ed1ac8": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/base/handlers.py#L1201", + "s52316685e1e4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L465-L516", + "s83f1081e863e": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/auth/identity.py#L255-L298", + "s6c884bdf8fb4": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L6-L26", + "sb7fab0e85c94": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/docs/source/operators/security.rst#L315-L322", + "s58a8a77857b5": "https://github.com/jupyter-server/jupyter_server/blob/v2.18.0/jupyter_server/services/api/api.yaml#L82-L137" + } + }, + "ollama": { + "name": "Ollama", + "basis": "v0.34.2", + "sources": { + "s96f77aee8bdd": "https://github.com/ollama/ollama/blob/v0.34.2/server/routes.go", + "sd146dc5d917d": "https://github.com/ollama/ollama/blob/v0.34.2/api/client.go", + "s7478dad099ab": "https://github.com/ollama/ollama/blob/v0.34.2/cmd/cmd.go", + "s2abe15b73bce": "https://github.com/ollama/ollama/blob/v0.34.2/envconfig/config.go" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.12.0", + "sources": { + "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", + "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", + "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", + "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375", + "s6c87acff18cf": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L358-L359", + "sf8fdfa16eb3f": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L6114-L6117", + "s1e96518d2ede": "https://github.com/iproute2/iproute2/blob/v6.12.0/misc/ss.c#L4235-L4240" + } + }, + "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": { + "publication": {"text": "Raw port publication or a firewall allowance adds no listener authentication; platform proxies authenticate only the routes they actually cover.", "components": ["runpod", "vast", "lambda", "modal"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "lambda:se22064748974", "modal:s7d083f819a21"], "status": "REASONED"}, + "runpod-http": {"text": "Expose HTTP Ports publishes https://[POD_ID]-[INTERNAL_PORT].proxy.runpod.net with automatic HTTPS; anyone with the URL can reach it, so the application still needs authentication.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "runpod-tcp": {"text": "Exposed TCP ports forward directly on a public IP without automatic TLS; secure template listeners before exposure and implement application TLS for sensitive TCP data.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "runpod-symmetry": {"text": "TCP configuration values above 70000 request symmetrical mapping rather than naming valid ports; read the assigned mapping from the pod environment, such as RUNPOD_TCP_PORT_70000. Mapping remains public.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "vast-ssh-port": {"text": "Vast.ai SSH launch mode opens internal port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-jupyter-port": {"text": "Vast.ai Jupyter launch mode opens internal 8080 as well as SSH port 22 by default.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-mapping": {"text": "Each open Vast.ai internal port maps to a random external port on a usually shared public IP; the networking docs describe no additional firewall step.", "components": ["vast"], "sources": ["vast:sc25989a1613e"], "status": "REASONED"}, + "vast-proxy": {"text": "Instance Portal uses local Caddy proxying when external and internal ports differ; forwarded apps can stay on loopback and PORTAL_CONFIG describes routing and presentation.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, + "vast-token": {"text": "Portal links carry a credential token; protect the URL, test access without it and confirm the selected route actually authenticates.", "components": ["vast"], "sources": ["vast:s0bdbda534723"], "status": "REASONED"}, + "vast-auth-controls": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the README documents OPEN_BUTTON_TOKEN for Basic/Bearer access, WEB_PASSWORD as a custom password, ENABLE_AUTH=false to disable authentication (default true), and AUTH_EXCLUDE as external ports excluded from authentication. The cited portal-aio configurator implements these; the auth test code records test expectations, not observed results.", "components": ["vast-image", "vast"], "sources": ["vast-image:sf75c3c07819e", "vast:s0bdbda534723", "vast-image:s5d2b91002bac", "vast-image:s229c9334547e", "vast-image:s3b735c4b3632", "vast-image:s037a7cbd778c", "vast-image:sdc669faa429d", "vast-image:sc77a7717bbc4", "vast-image:s3c43b58f9e41", "vast-image:sf1b910a375c6", "vast-image:sd9c32e411f08", "vast-image:sea35d951abe5", "vast-image:s4a05112af859"], "status": "REASONED"}, + "vast-portal-routing": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, portal-aio reads PORTAL_CONFIG only when /etc/portal.yaml is absent, writes it back there, and emits a Caddy listener only where external and internal ports differ and the external port has a VAST_TCP_PORT mapping; other entries bypass proxying with no portal authentication. In this guide's assessment, routing configuration is not a credential.", "components": ["vast-image"], "sources": ["vast-image:sd0ba21945c9a", "vast-image:sd9c32e411f08"], "status": "REASONED"}, + "vast-portal-jupyter": {"text": "Boot edits PORTAL_CONFIG: absent/empty 8080 mapping drops Jupyter; non-Jupyter /.launch drops it and Jupyter /.launch rewrites 8080:18080 to equal ports unless JUPYTER_OVERRIDE=true (case-insensitive). Re-add needs no /.launch, a nonempty mapping, no Jupyter entry and FORCE_JUPYTER not false (case-insensitive); /etc/portal.yaml wins, so only effective equal-port entries lose portal authentication.", "components": ["vast-image"], "sources": ["vast-image:s10acc068e127", "vast-image:sd9c32e411f08", "vast-image:sd0ba21945c9a"], "status": "REASONED"}, + "vast-auth-defaults": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator keeps authentication on unless ENABLE_AUTH equals false case-insensitively, serves plain HTTP unless ENABLE_HTTPS is true with valid certs, defaults WEB_USERNAME to vastai, and gives WEB_PASSWORD and OPEN_BUTTON_TOKEN no precedence: an unset one mirrors the other, and one generated value serves both when neither is set.", "components": ["vast-image"], "sources": ["vast-image:s3c43b58f9e41"], "status": "REASONED"}, + "vast-auth-routes": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, a protected port accepts either token as a token query parameter, a container-label auth cookie or an Authorization: Bearer header, sets the cookie on a valid query token, and falls back to HTTP Basic whose hashed password is WEB_PASSWORD.", "components": ["vast-image"], "sources": ["vast-image:sf1b910a375c6", "vast-image:s4a05112af859", "vast-image:sea35d951abe5"], "status": "REASONED"}, + "vast-auth-exemptions": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, AUTH_EXCLUDE ports, matched externally, and every port under ENABLE_AUTH=false are proxied with no authentication, an exposure. Even on authenticated ports, /portal-resolver answers anonymously and ACME challenge, change-password, manifest and security.txt paths proxy to the backend with no credential, an exposure for private content there.", "components": ["vast-image"], "sources": ["vast-image:sc77a7717bbc4", "vast-image:sd9c32e411f08", "vast-image:s5a43e0cf8348", "vast-image:sae1085635ff9", "vast-image:s4a05112af859"], "status": "REASONED"}, + "vast-token-reuse": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the configurator prints the effective credentials to its console output, and OPEN_BUTTON_TOKEN is also the Syncthing GUI API key in the launcher and supervisor scripts (the supervisor's random fallback only when unset or empty). In this guide's reasoning, sharing a token-bearing link shares these reused credentials too.", "components": ["vast-image"], "sources": ["vast-image:s5c18e60bb9e7", "vast-image:s92be3bd5cdef", "vast-image:s7ac7955e604d"], "status": "REASONED"}, + "vast-manifest-report": {"text": "At base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, manifest reporting and configurator enforcement lowercase ENABLE_AUTH and default unset to \"true\": reporting tests equality to \"true\", enforcement inequality to \"false\". Other values can be reported disabled yet enforced; the manifest reports, it does not enforce.", "components": ["vast-image"], "sources": ["vast-image:s6d0bc9f2a0da", "vast-image:s3c43b58f9e41"], "status": "REASONED"}, + "lambda-default": {"text": "Lambda inbound firewall denies by default except ICMP and TCP 22; other listeners require explicit global/workspace rules or a per-instance ruleset attached at launch.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, + "lambda-rule": {"text": "An allow rule admits the specified source range without authenticating callers; pair it with listener or proxy authentication. Removing an allow rule closes access.", "components": ["lambda"], "sources": ["lambda:se22064748974"], "status": "REASONED"}, + "modal-web": {"text": "Web endpoints using modal.fastapi_endpoint, the replacement for @web_endpoint, are public by default; requires_proxy_auth=True enables proxy authentication.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-dedicated": {"text": "Dedicated Endpoints require proxy authentication by default; --unauthenticated makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-server": {"text": "Servers require proxy authentication by default; @app.server(unauthenticated=True) makes them public.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "modal-shared": {"text": "Shared Endpoints always require a Proxy Token; the cited pages document the --unauthenticated option for Dedicated Endpoints only, so, in this guide's assessment, a Shared Endpoint cannot be made public.", "components": ["modal"], "sources": ["modal:s7d083f819a21", "modal:s12a4f5e04dbe", "modal:s33e1c2c2faf3"], "status": "REASONED"}, + "modal-credentials": {"text": "Proxy authentication accepts Modal-Key and Modal-Secret or Authorization: Bearer .; missing credentials on a protected endpoint return 401.", "components": ["modal"], "sources": ["modal:s7d083f819a21"], "status": "REASONED"}, + "runpod-bind": {"text": "RunPod HTTP proxy needs the exposed pod interface: bind the exposed service to 0.0.0.0 inside the pod, not localhost.", "components": ["runpod"], "sources": ["runpod:s817198f039ee"], "status": "REASONED"}, + "listener-auth": {"text": "Whichever binding the platform's proxy needs, add the listener's OWN authentication where it supports one; otherwise keep the unauthenticated backend private and put an authenticating gateway in front, so the platform exposes the gateway rather than the bare listener. At Ollama v0.34.2, the cited local API route setup installs CORS and host filtering without an inbound credential check. Cloud sign-in and OLLAMA_AUTH client outbound signing are separate from local inbound authentication.", "components": ["runpod", "vast", "jupyter", "ollama"], "sources": ["runpod:s817198f039ee", "vast:s0bdbda534723", "jupyter:sc703a8030c8f", "ollama:s96f77aee8bdd", "ollama:sd146dc5d917d", "ollama:s7478dad099ab", "ollama:s2abe15b73bce"], "status": "REASONED"}, + "mfa": {"text": "MFA: this guide cites no platform feature that adds a second factor to the workload itself, so workload authentication must be verified separately. This guide's advice is to enable MFA on each RunPod, Vast.ai, Lambda, and Modal platform account; no account-security source for these platforms is cited here. In this guide's reasoning, an API key or proxy-auth token secures a machine client and is a separate control from the platform login.", "components": ["modal", "jupyter"], "sources": ["modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED"}, + "lambda-ssh": {"text": "Lambda requires an SSH key at launch; keep the private key off the instance.", "components": ["lambda"], "sources": ["lambda:sb12b4a953833"], "status": "REASONED"}, + "vast-ssh": {"text": "Vast.ai disables SSH password authentication and uses the registered public key; keep the private key off the instance.", "components": ["vast"], "sources": ["vast:s6e0303ef19fe"], "status": "REASONED"}, + "runpod-ssh": {"text": "RunPod recommends SSH keys and offers an optional password; use the key and skip the password.", "components": ["runpod"], "sources": ["runpod:s13c43b856d4f"], "status": "REASONED"}, + "verify-inventory": {"text": "This guide infers from the ss namespace switch and Linux namespace isolation that ss -tlnp inventories TCP listeners only in the current network namespace, assuming normal live kernel/proc inputs without input-file environment overrides. This is not a UDP, firewall-state or platform-publication check; this guide's test design requires cross-checking platform mappings and probing each mapped port from outside.", "components": ["runpod", "vast", "iproute2", "network-namespaces"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850", "iproute2:s6c87acff18cf", "iproute2:sf8fdfa16eb3f", "iproute2:s1e96518d2ede", "network-namespaces:s427c923e346f"], "status": "REASONED", "verify": [1]}, + "verify-jupyter": {"text": "This guide's test design compares anonymous and valid-token requests directly to loopback :8888/api/contents. For Jupyter Server v2.18.0 with token authentication enabled, no session cookie, the route at that URL and an existing readable contents path, anonymous GET returns 403 without a login redirect; with default authorization, valid-token GET completes the JSON model. The REST specification documents HTTP 200 for this successful GET. Any anonymous data response is a finding, in this guide's assessment. Tokens are accepted in Authorization: token or the token query parameter; password-only and cookie-authenticated requests need separate controls.", "components": ["jupyter", "jupyter-contents"], "sources": ["jupyter:sc703a8030c8f", "jupyter-contents:sdc3ad9152505", "jupyter-contents:sf0a3ea8ee42e", "jupyter-contents:s4bdc7b9183f3", "jupyter-contents:s4e7504ed1ac8", "jupyter-contents:s52316685e1e4", "jupyter-contents:s83f1081e863e", "jupyter-contents:s6c884bdf8fb4", "jupyter-contents:sb7fab0e85c94", "jupyter-contents:s58a8a77857b5"], "status": "REASONED", "verify": [1]}, + "verify-mapping": {"text": "Repeat credential-free and authenticated requests per exposed mapping; anonymous app data fails, while a proxy 401 alone does not prove native listener auth and needs the direct positive control.", "components": ["runpod", "vast", "modal", "jupyter"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast:s0bdbda534723", "modal:s7d083f819a21", "jupyter:sc703a8030c8f"], "status": "REASONED", "verify": [1]}, + "verify-portal-exempt": {"text": "For an authenticated portal mapping at the cited commit, this guide expects resolver 200, exempt-path backend responses and a Basic challenge elsewhere, distinguishable only with a portal header known to differ from the backend. Anonymous known private content is a finding when that path is private; transport failures, backend 401s and unexplained responses are INCONCLUSIVE.", "components": ["vast-image"], "sources": ["vast-image:sae1085635ff9", "vast-image:s4a05112af859", "vast-image:sd9c32e411f08", "vast-image:s3c43b58f9e41"], "status": "REASONED", "verify": [2]}, + "remote-desktop": {"text": "Overlooking a template's remote desktop: at Vast.ai base-image commit 00064421641881c1f83ff58c55f13cbc10cbea4d, the linux-desktop launcher configures x11vnc on port 5900 with a password from VNC_PASSWORD, falling back to OPEN_BUTTON_TOKEN when VNC_PASSWORD is unset or empty. The template README describes OPEN_BUTTON_TOKEN as randomly generated on first boot and visible in instance logs, and offers Selkies WebRTC and Guacamole browser VNC alongside direct VNC. These citations do not establish other templates' ports or password defaults, or the strength of the effective VNC password. This guide's advice: check both browser and direct VNC listeners and their public mappings for missing or weak authentication. In this guide's reasoning, the browser page's HTTPS does not protect a separately reachable VNC connection, so a direct VNC mapping needs its own strong password and an encrypted tunnel, or should be kept off the public mapping. This guide's advice: test a direct VNC mapping with a VNC client, since the browser access controls and the HTTP status checks above may not cover it.", "components": ["runpod", "vast", "vast-image"], "sources": ["runpod:s817198f039ee", "vast:sc25989a1613e", "vast-image:s0428d55e9145", "vast-image:s1b8c6c46f358", "vast-image:s44f3439f13de"], "status": "REASONED"} + } +} +```` +====================================================================== +==> deployment-lifecycle.md +====================================================================== + +# Deploying safely over time: verify from outside, safe order, previews, teardown + +Written against Nmap documentation (no recorded version), BIND dig documentation 9.18.39, OpenSSL command documentation 3.0, OpenBSD nc manual 7.6, OWASP Web Security Testing Guide v4.2, OAuth2 Proxy v7.15.4, Authelia v4.39.28, and NIST digital identity guidelines SP 800-63B-4, plus 2 more recorded bases; 9 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 22 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. + This repository's per-service guides check TLS, binding, and authentication from inside the host and at the moment you set them up. Real exposure is judged from outside the host, and it drifts: a preview URL, a restored backup, or an upgrade can reopen something already closed. This guide is the lifecycle layer around the per-service checks. ## 1. Verify from outside, not just from the host @@ -18722,6 +20213,323 @@ openssl s_client -connect app.example.com:443 -servername app.example.com \ - Certbot staging-server selection and custom-server exception, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L237-L264 - Certbot user-guide dry-run and subsequent live-renewal example, at v2.3.0 (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L786-L798 + + +## 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/deployment-lifecycle.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): + +- nmap: Nmap documentation (unknown) +- shodan: Shodan (unknown) +- censys: Censys (unknown) +- vercel: Vercel Deployment Protection (unknown) +- access: Cloudflare Access policies (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- dns: BIND dig documentation (9.18.39) +- openssl: OpenSSL command documentation (3.0) +- openbsd-nc: OpenBSD nc manual (7.6) +- owasp-takeover: OWASP subdomain takeover prevention (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) +- wstg: OWASP Web Security Testing Guide (v4.2) +- dns-rfcs: DNS caching RFCs (unknown) +- owasp-docs: OWASP security guidance (668ba7db3d0da5868b8a0305c259f7f6914a6ecd) +- entra-docs: Microsoft Entra revocation model (231747abc59ae3d50c74e215c1cdd592eb980723) +- cloudflare-access: Cloudflare Access and origin configuration (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- oauth2-proxy: OAuth2 Proxy (v7.15.4) +- authelia: Authelia (v4.39.28) +- oauth-revocation: OAuth 2.0 Token Revocation RFC (unknown) +- nist: NIST digital identity guidelines (SP 800-63B-4) +- ubuntu-nc: Ubuntu noble netcat-openbsd source (c047f1a4572e796c8643fb7e5a719a9d960a677b) +- c9s-nmap: CentOS Stream 9 nmap packaging (17c2b7795d821cd7fbe311cf3f885be3ea1f122f) +- busybox: BusyBox 1.36.1 (1_36_1) +- docker-notes: Docker Engine release notes (4e9a5751518ed8223a8dcde53693badddd72604f) +- webui: Open WebUI (0a7c15832fb30b1903753e83f81dc7d27e5b0944) +- certbot-v230: Certbot user guide and source (v2.3.0) +- le: Let's Encrypt documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- outside (REASONED; nmap): Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour. +- tcp-scan (REASONED; nmap, openbsd-nc, ubuntu-nc, c9s-nmap, busybox): Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive. +- udp-scan (REASONED; nmap): The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU. +- all-addresses (REASONED; nmap, vercel): Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others. +- passive-index (REASONED; shodan, censys): Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan. +- initialize-secrets (REASONED; access, webui): As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here. +- registration (REASONED; access, webui): As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here. +- fronting-order (REASONED; access, le): As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice. +- setup-route (REASONED; access, webui): After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only. +- preview-standard (REASONED; vercel): Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open. +- preview-all (REASONED; vercel): All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on. +- preview-gate (REASONED; access, vercel): Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs. +- clone-secrets (REASONED; access): As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only. +- dns-retirement (REASONED; dns, owasp-takeover, wstg, dns-rfcs): Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection. +- retire-credentials (REASONED; access, owasp-docs): After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users. +- reverify (REASONED; nmap, docker-notes, webui): Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only. +- certificate-monitor (REASONED; nmap, openssl, certbot-v230): Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only. +- offboarding (REASONED; access, owasp-docs, entra-docs, cloudflare-access, oauth2-proxy, authelia, oauth-revocation, nist): When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations. +- verify-ipv4 (REASONED; nmap): From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive. +- verify-ipv6 (REASONED; nmap): Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive. +- verify-dns (REASONED; dns, dns-rfcs): curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive. +- verify-certificate (REASONED; nmap, openssl): The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "2b1579a1516959fa861ec6c8982f8581b5cba283120096bc4a10bdcf25d10c1b", + "components": { + "nmap": { + "name": "Nmap documentation", + "basis": "unknown", + "sources": { + "sf8404f695ccd": "https://nmap.org/book/man-briefoptions.html", + "sb7d0e7eb5141": "https://nmap.org/book/man-host-discovery.html" + } + }, + "shodan": { + "name": "Shodan", + "basis": "unknown", + "sources": { + "scf863db67f1e": "https://www.shodan.io/" + } + }, + "censys": { + "name": "Censys", + "basis": "unknown", + "sources": { + "s1e64a007a91f": "https://censys.com/" + } + }, + "vercel": { + "name": "Vercel Deployment Protection", + "basis": "unknown", + "sources": { + "se6089d5c4e96": "https://vercel.com/docs/deployment-protection", + "s7836e043434a": "https://vercel.com/changelog/protect-production-deployments-for-free-on-every-plan" + } + }, + "access": { + "name": "Cloudflare Access policies", + "basis": "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" + } + }, + "dns": { + "name": "BIND dig documentation", + "basis": "9.18.39", + "sources": { + "s54cff06bb882": "https://bind9.readthedocs.io/en/v9.18.39/manpages.html" + } + }, + "openssl": { + "name": "OpenSSL command documentation", + "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/" + } + }, + "openbsd-nc": { + "name": "OpenBSD nc manual", + "basis": "7.6", + "sources": { + "s0c8b1a738e7b": "https://man.openbsd.org/OpenBSD-7.6/nc.1" + } + }, + "owasp-takeover": { + "name": "OWASP subdomain takeover prevention", + "basis": "668ba7db3d0da5868b8a0305c259f7f6914a6ecd", + "sources": { + "s03e38fd5f89d": "https://github.com/OWASP/CheatSheetSeries/blob/668ba7db3d0da5868b8a0305c259f7f6914a6ecd/cheatsheets/Subdomain_Takeover_Prevention_Cheat_Sheet.md#L5", + "sf5a4befe8112": "https://github.com/OWASP/CheatSheetSeries/blob/668ba7db3d0da5868b8a0305c259f7f6914a6ecd/cheatsheets/Subdomain_Takeover_Prevention_Cheat_Sheet.md#L43-L44", + "s33c539162561": "https://github.com/OWASP/CheatSheetSeries/blob/668ba7db3d0da5868b8a0305c259f7f6914a6ecd/cheatsheets/Subdomain_Takeover_Prevention_Cheat_Sheet.md#L98-L105" + } + }, + "wstg": { + "name": "OWASP Web Security Testing Guide", + "basis": "v4.2", + "sources": { + "s62c309021e80": "https://github.com/OWASP/wstg/blob/v4.2/document/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/10-Test_for_Subdomain_Takeover.md#L9-L14", + "s39a0e1d42b51": "https://github.com/OWASP/wstg/blob/v4.2/document/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/10-Test_for_Subdomain_Takeover.md#L109" + } + }, + "dns-rfcs": { + "name": "DNS caching RFCs", + "basis": "unknown", + "sources": { + "s0d5c3bb5aad8": "https://www.rfc-editor.org/rfc/rfc1034#section-3.6", + "s8bc007a2299d": "https://www.rfc-editor.org/rfc/rfc1035#section-3.2.1", + "s3b645146ca75": "https://www.rfc-editor.org/rfc/rfc2181#section-8", + "s78f445a994d7": "https://www.rfc-editor.org/rfc/rfc8767#section-4", + "s1d32db254435": "https://www.rfc-editor.org/rfc/rfc2308#section-2" + } + }, + "owasp-docs": { + "name": "OWASP security guidance", + "basis": "668ba7db3d0da5868b8a0305c259f7f6914a6ecd", + "sources": { + "seb866a1f94e9": "https://github.com/OWASP/CheatSheetSeries/blob/668ba7db3d0da5868b8a0305c259f7f6914a6ecd/cheatsheets/Secrets_Management_Cheat_Sheet.md#L214-L218" + } + }, + "entra-docs": { + "name": "Microsoft Entra revocation model", + "basis": "231747abc59ae3d50c74e215c1cdd592eb980723", + "sources": { + "s983f45259faa": "https://github.com/MicrosoftDocs/entra-docs/blob/231747abc59ae3d50c74e215c1cdd592eb980723/docs/identity/users/users-revoke-access.md#L62", + "scb38e683ae3f": "https://github.com/MicrosoftDocs/entra-docs/blob/231747abc59ae3d50c74e215c1cdd592eb980723/docs/identity/users/users-revoke-access.md#L96-L106", + "s9ff4d6ed4cfe": "https://github.com/MicrosoftDocs/entra-docs/blob/231747abc59ae3d50c74e215c1cdd592eb980723/docs/identity-platform/access-tokens.md#L54-L60", + "sca6feea4b601": "https://github.com/MicrosoftDocs/entra-docs/blob/231747abc59ae3d50c74e215c1cdd592eb980723/docs/identity/conditional-access/concept-continuous-access-evaluation.md#L21-L25" + } + }, + "cloudflare-access": { + "name": "Cloudflare Access and origin configuration", + "basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104", + "sources": { + "sf2dbab0fc574": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/session-management.mdx#L159-L186", + "sa0e3eb928f4a": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/session-management.mdx#L190-L195" + } + }, + "oauth2-proxy": { + "name": "OAuth2 Proxy", + "basis": "v7.15.4", + "sources": { + "s32cedbc14f60": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L126-L130" + } + }, + "authelia": { + "name": "Authelia", + "basis": "v4.39.28", + "sources": { + "sc844fe82794f": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/session/introduction.md#L220-L221" + } + }, + "oauth-revocation": { + "name": "OAuth 2.0 Token Revocation RFC", + "basis": "unknown", + "sources": { + "s3383c9f41cc2": "https://www.rfc-editor.org/rfc/rfc7009.html#section-2.1" + } + }, + "nist": { + "name": "NIST digital identity guidelines", + "basis": "SP 800-63B-4", + "sources": { + "s2a689905c161": "https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b-4.pdf" + } + }, + "ubuntu-nc": { + "name": "Ubuntu noble netcat-openbsd source", + "basis": "c047f1a4572e796c8643fb7e5a719a9d960a677b", + "sources": { + "s478aaf9a8975": "https://git.launchpad.net/ubuntu/+source/netcat-openbsd/tree/nc.1?id=c047f1a4572e796c8643fb7e5a719a9d960a677b#n257", + "sd7b31d3bc579": "https://git.launchpad.net/ubuntu/+source/netcat-openbsd/tree/nc.1?id=c047f1a4572e796c8643fb7e5a719a9d960a677b#n263", + "scc48da80246b": "https://git.launchpad.net/ubuntu/+source/netcat-openbsd/tree/nc.1?id=c047f1a4572e796c8643fb7e5a719a9d960a677b#n308" + } + }, + "c9s-nmap": { + "name": "CentOS Stream 9 nmap packaging", + "basis": "17c2b7795d821cd7fbe311cf3f885be3ea1f122f", + "sources": { + "sd06e83963e39": "https://gitlab.com/redhat/centos-stream/rpms/nmap/-/blob/17c2b7795d821cd7fbe311cf3f885be3ea1f122f/nmap.spec#L133-L137", + "s3ab25d24e37a": "https://gitlab.com/redhat/centos-stream/rpms/nmap/-/blob/17c2b7795d821cd7fbe311cf3f885be3ea1f122f/nmap.spec#L6-L8" + } + }, + "busybox": { + "name": "BusyBox 1.36.1", + "basis": "1_36_1", + "sources": { + "s34bdee292f79": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc_bloaty.c#L61-L93", + "sd707e18625cc": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L136-L170", + "s14b89daa008d": "https://github.com/mirror/busybox/blob/1_36_1/networking/nc.c#L30-L59" + } + }, + "docker-notes": { + "name": "Docker Engine release notes", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "s182da2bdd442": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L771-L772", + "sfbe11311e8a4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/release-notes/28.md#L263-L263" + } + }, + "webui": { + "name": "Open WebUI", + "basis": "0a7c15832fb30b1903753e83f81dc7d27e5b0944", + "sources": { + "s94d055784840": "https://raw.githubusercontent.com/open-webui/open-webui/0a7c15832fb30b1903753e83f81dc7d27e5b0944/backend/open_webui/config.py", + "s9d6f5fb157de": "https://raw.githubusercontent.com/open-webui/open-webui/0a7c15832fb30b1903753e83f81dc7d27e5b0944/backend/open_webui/routers/auths.py" + } + }, + "certbot-v230": { + "name": "Certbot user guide and source", + "basis": "v2.3.0", + "sources": { + "s5def4225771f": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/__init__.py#L146-L170", + "se96df86ba351": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L210-L211", + "sc37d1ffd6192": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/certbot/_internal/cli/helpful.py#L237-L264", + "s77de4441a9e5": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L786-L798" + } + }, + "le": { + "name": "Let's Encrypt documentation", + "basis": "unknown", + "sources": { + "s9ff2d787c618": "https://letsencrypt.org/docs/" + } + } + }, + "claims": { + "outside": {"text": "Run exposure checks from a second network; host-local curl and ss do not establish outside reachability or platform firewall behaviour.", "components": ["nmap"], "sources": ["nmap:sf8404f695ccd", "nmap:sb7d0e7eb5141"], "status": "REASONED"}, + "tcp-scan": {"text": "Sweep authorized hosts with nmap -Pn -p-; the guide also gives an nc port loop. Use the house placeholder guard for both. Ubuntu noble netcat-openbsd documents -v verbosity, -z scanning and -w timeout; BusyBox 1.36.1 small nc lacks -v/-z and needs NC_EXTRA for -w, while its NC_110_COMPAT applet lists -v/-w and needs NC_EXTRA for -z. CentOS Stream 9 packages Ncat 7.92 as nc; its option behaviour and traditional/GNU netcat remain unestablished. As this guide's assessment, refusal or timeout shows only connection failure; unsupported-option errors, local errors and silent exits are inconclusive.", "components": ["nmap", "openbsd-nc", "ubuntu-nc", "c9s-nmap", "busybox"], "sources": ["nmap:sf8404f695ccd", "nmap:sb7d0e7eb5141", "openbsd-nc:s0c8b1a738e7b", "ubuntu-nc:s478aaf9a8975", "ubuntu-nc:sd7b31d3bc579", "ubuntu-nc:scc48da80246b", "c9s-nmap:sd06e83963e39", "c9s-nmap:s3ab25d24e37a", "busybox:s34bdee292f79", "busybox:sd707e18625cc", "busybox:s14b89daa008d"], "status": "REASONED"}, + "udp-scan": {"text": "The full port sweep is TCP only; inventory UDP services and probe their ports separately with nmap -sU.", "components": ["nmap"], "sources": ["nmap:sf8404f695ccd"], "status": "REASONED"}, + "all-addresses": {"text": "Check public IPv4, public IPv6 and platform URLs as well as the custom domain; one address cannot establish coverage of the others.", "components": ["nmap", "vercel"], "sources": ["nmap:sf8404f695ccd", "vercel:se6089d5c4e96"], "status": "REASONED"}, + "passive-index": {"text": "Cross-reference Shodan and Censys indexes for previously indexed exposure; indexed observations are not a fresh direct scan.", "components": ["shodan", "censys"], "sources": ["shodan:scf863db67f1e", "censys:s1e64a007a91f"], "status": "REASONED"}, + "initialize-secrets": {"text": "As this guide's advice, create owner/admin credentials and any session, JWT or cookie-signing secrets before opening application access, following authentication.md's signing-secret recommendation. For applications that give admin to whoever completes first-run setup, an unclaimed setup wizard is a race to become admin; Open WebUI at the recorded pin is one example, with its first account becoming admin. Access is fronting context only; other applications' bootstrap and signing-secret behaviour are not established here.", "components": ["access", "webui"], "sources": ["access:s7cafe1b17afd", "webui:s94d055784840", "webui:s9d6f5fb157de"], "status": "REASONED"}, + "registration": {"text": "As this guide's advice, disable open self-registration or restrict it to an allowlisted domain before opening application access; authentication.md covers allowlisting after federated login. Open WebUI at the recorded pin defaults ENABLE_SIGNUP and ENABLE_LOGIN_FORM to true with authentication enabled; an empty database bypasses ENABLE_SIGNUP when the login form is enabled, and after accounts exist disabled signup refuses registration. Access is fronting context only; other applications' registration controls and domain enforcement are not established here.", "components": ["access", "webui"], "sources": ["access:s7cafe1b17afd", "webui:s94d055784840", "webui:s9d6f5fb157de"], "status": "REASONED"}, + "fronting-order": {"text": "As this guide's advice, install TLS and any proxy authentication before opening application access: use DNS-01 before opening inbound access, or open only the challenge port during issuance while keeping application access blocked. Per free-certificates.md, HTTP-01 requires inbound port 80 and TLS-ALPN-01 requires inbound port 443; DNS-01 needs no inbound port. These challenge requirements do not establish this deployment order. Cloudflare recommends creating Access before the tunnel route and warns that without it the published app is public; publicly routable origins need separate IP protection. The general order remains this guide's advice.", "components": ["access", "le"], "sources": ["access:s7cafe1b17afd", "access:sfddb92e438ca", "le:s9ff2d787c618"], "status": "REASONED"}, + "setup-route": {"text": "After initialization, restart and retry the initial-setup URL; it must refuse to mint a second admin, not silently succeed (this guide's test expectation). Open WebUI's signup handler illustrates first-account bootstrap and refusal when accounts exist and signup is disabled; it does not establish every application's setup route or a deployed restart result. Access is fronting context only.", "components": ["access", "webui"], "sources": ["access:s7cafe1b17afd", "webui:s9d6f5fb157de"], "status": "REASONED"}, + "preview-standard": {"text": "Vercel Standard Protection is available on every plan and gates previews and generated deployment URLs while leaving the production domain open.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96"], "status": "REASONED"}, + "preview-all": {"text": "All Deployments also protects production; pairing it with Vercel Authentication became free on every plan on 9 September 2026, with no paid add-on.", "components": ["vercel"], "sources": ["vercel:se6089d5c4e96", "vercel:s7836e043434a"], "status": "REASONED"}, + "preview-gate": {"text": "Apply a fronting gate such as a Cloudflare Access policy to preview hostnames the platform gate does not cover. The pinned Cloudflare instructions establish the Access gate, not the platform's coverage of preview URLs.", "components": ["access", "vercel"], "sources": ["access:s7cafe1b17afd", "vercel:se6089d5c4e96"], "status": "REASONED"}, + "clone-secrets": {"text": "As this guide's advice, treat a clone's data the same as the original and rotate any inherited credential if the clone is less trusted than the source. That a clone can inherit credentials is this guide's assessment; clone credential inheritance and rotation are not established by the Access citation, which supplies fronting context only.", "components": ["access"], "sources": ["access:s7cafe1b17afd"], "status": "REASONED"}, + "dns-retirement": {"text": "Remove CNAME or A/AAAA records before releasing the app, load balancer or IP, and wait at least the old TTL per OWASP; WSTG supports record removal, not that wait. TTL expiry is not a hard bound: RFC 8767 permits serve-stale when authoritative refresh fails. This guide advises retaining the resource through the old TTL and confirmation of removal at every authoritative nameserver, and accounting for serve-stale before release; these checks do not prove all caches have refreshed. By this guide's inference, deletion does not flush cached answers and cached clients can reach the next owner of a reclaimable target. Reclaimability is OWASP's account of claimable CNAME targets and reassigned released IPs, not established for a particular platform. dig supports DNS inspection.", "components": ["dns", "owasp-takeover", "wstg", "dns-rfcs"], "sources": ["dns:s54cff06bb882", "owasp-takeover:s03e38fd5f89d", "owasp-takeover:sf5a4befe8112", "owasp-takeover:s33c539162561", "wstg:s62c309021e80", "wstg:s39a0e1d42b51", "dns-rfcs:s0d5c3bb5aad8", "dns-rfcs:s8bc007a2299d", "dns-rfcs:s3b645146ca75", "dns-rfcs:s78f445a994d7"], "status": "REASONED"}, + "retire-credentials": {"text": "After DNS retirement revoke deployment access policies, API/service credentials and dedicated database users (guide requirement). OWASP requires secure revocation of secrets that are no longer required or potentially compromised. The Access-policy citation covers fronting context only; the cited files do not establish removal of an Access application or policy or dropping database users.", "components": ["access", "owasp-docs"], "sources": ["access:s7cafe1b17afd", "owasp-docs:seb866a1f94e9"], "status": "REASONED"}, + "reverify": {"text": "Repeat outside scans and negative auth tests after upgrades, backup restores or network/auth changes; a restore can reintroduce a default account or reset a feature flag as this guide's assessment, not a general vendor fact. Docker 28.0.0 warns that hosts with IPv6 forwarding already enabled that relied on Docker setting FORWARD to DROP may need configuration changes; 28.3.3 fixes local-network access to loopback-intended published ports after firewalld reload. Open WebUI enables persistence by default; as this guide's conditional inference, restoring database values that override the environment after first launch could reinstate earlier signup settings. The files this guide cites do not establish database precedence or general restore behaviour; Nmap supports scanning only.", "components": ["nmap", "docker-notes", "webui"], "sources": ["nmap:sf8404f695ccd", "nmap:sb7d0e7eb5141", "docker-notes:s182da2bdd442", "docker-notes:sfbe11311e8a4", "webui:s94d055784840"], "status": "REASONED"}, + "certificate-monitor": {"text": "Monitor certificate expiry externally. As this guide's assessment, a renewal cron can silently fail while the host's view looks fine; its advice is a scheduled external openssl s_client or third-party monitor. Certbot v2.3.0 dry-run uses staging unless a custom server is set, saves no test certificates and runs applicable deploy hooks only with --run-deploy-hooks after success, using the active certificate. The user guide separates dry-run from live renewal; this guide infers that dry-run cannot establish the last real renewal's success. Nmap is outside-check context only.", "components": ["nmap", "openssl", "certbot-v230"], "sources": ["nmap:sf8404f695ccd", "openssl:s00aaf106164f", "openssl:s0856f4a58574", "certbot-v230:s5def4225771f", "certbot-v230:se96df86ba351", "certbot-v230:sc37d1ffd6192", "certbot-v230:s77de4441a9e5"], "status": "REASONED"}, + "offboarding": {"text": "When a person leaves, revoke every touched layer, not only the password: identity-provider membership, proxy/app sessions on each service, personal tokens and API keys (this guide's recommendation), and revoke the second factor alongside the account. Entra documents blocking sign-ins and revoking refresh tokens; application sessions require application revocation, access-token lifetimes vary, and supported-service CAE critical-event enforcement can take up to 15 minutes. Cloudflare documents per-user revocation, but an active IdP profile can start a new session; user logout stops acceptance of issued tokens in 20-30 seconds. OAuth2 Proxy documents cookie expiry/refresh; Authelia documents cookie expiry and session destruction, overridden by remember-me; neither passage establishes immediate per-user revocation. RFC 7009 specifies immediate invalidation with possible propagation delay and conditional access-token revocation support. OWASP requires secure revocation of unneeded or potentially compromised secrets. NIST requires CSPs to promptly invalidate authenticators when the CSP determines that the subscriber no longer meets its eligibility requirements. Where immediate revocation is unavailable, measure and shrink maximum time-to-revoke (this guide's recommendation). Provider-membership removal, every application's session-revocation mechanism and this deployment's measured maximum time-to-revoke remain unestablished. Follow authentication.md's REASONED replay check: matched requests must succeed before revocation, then old credentials must be refused without protected data within each configured maximum; these are test expectations, not observations.", "components": ["access", "owasp-docs", "entra-docs", "cloudflare-access", "oauth2-proxy", "authelia", "oauth-revocation", "nist"], "sources": ["access:s7cafe1b17afd", "owasp-docs:seb866a1f94e9", "entra-docs:s983f45259faa", "entra-docs:scb38e683ae3f", "entra-docs:s9ff4d6ed4cfe", "entra-docs:sca6feea4b601", "cloudflare-access:sf2dbab0fc574", "cloudflare-access:sa0e3eb928f4a", "oauth2-proxy:s32cedbc14f60", "authelia:sc844fe82794f", "oauth-revocation:s3383c9f41cc2", "nist:s2a689905c161"], "status": "REASONED"}, + "verify-ipv4": {"text": "From another network, nmap -Pn -p- should find only intended TCP ports; unintended open ports fail, and errors or an unexecuted scan are inconclusive.", "components": ["nmap"], "sources": ["nmap:sf8404f695ccd", "nmap:sb7d0e7eb5141"], "status": "REASONED", "verify": [1]}, + "verify-ipv6": {"text": "Repeat the full TCP sweep over the public IPv6 address with -6; a scan that did not run is inconclusive.", "components": ["nmap"], "sources": ["nmap:sf8404f695ccd", "nmap:sb7d0e7eb5141"], "status": "REASONED", "verify": [1]}, + "verify-dns": {"text": "curl failure does not prove DNS removal; resolver A/AAAA/CNAME answers need investigation and empty output is inconclusive. Check every authoritative server with +norecurse for NXDOMAIN or NOERROR/NODATA without CNAME; SERVFAIL, referral and timeout are inconclusive.", "components": ["dns", "dns-rfcs"], "sources": ["dns:s54cff06bb882", "dns-rfcs:s1d32db254435"], "status": "REASONED", "verify": [1]}, + "verify-certificate": {"text": "The scheduled external s_client probe checks hostname and verification errors and pipes the certificate to x509 -enddate. No observed outcome is recorded; Nmap is outside-check context only.", "components": ["nmap", "openssl"], "sources": ["nmap:sf8404f695ccd", "openssl:s00aaf106164f", "openssl:s0856f4a58574"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> postgresql.md ====================================================================== @@ -34043,74 +35851,9 @@ Claims (full source mapping in the JSON block below): # SQLite in deployment: the file is the exposure (plus Turso and Litestream) - -**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): - -- see: SQLite Encryption Extension (unknown) -- cipher: SQLCipher (unknown) -- turso: Turso documentation (unknown) -- litestream: Litestream documentation (unknown) -- mcp: Litestream MCP minimum (v0.5.0) -- litefs: LiteFS source (v0.5.14) -- litefs-docs: LiteFS documentation (unknown) -- go: Go library documentation (unknown) -- docker: Docker documentation (unknown) -- sqlite-rolling: SQLite documentation (rolling) (unknown) -- git: Git documentation (v2.56.0) -- coreutils: GNU Coreutils stat manual (v9.7) -- bash: GNU Bash reference manual (bash-5.3) -- curl: curl (curl-8_14_1) -- grep-docs: GNU grep documentation (v3.12) - -Claims (full source mapping in the front matter): +Written against SQLite documentation (rolling) (no recorded version), Litestream MCP minimum v0.5.0, LiteFS source v0.5.14, Git documentation v2.56.0, GNU Coreutils stat manual v9.7, GNU Bash reference manual bash-5.3, curl curl-8_14_1, and GNU grep documentation v3.12; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 40 claims: 2 demonstrated by a check that ran and 38 reasoned from cited sources. -- filesystem (REASONED; sqlite-rolling): No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect. -- extensions (REASONED; sqlite-rolling): Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries. -- web-files (REASONED; sqlite-rolling): Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names. -- git-files (REASONED; sqlite-rolling, git): Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit. -- permissions (REASONED; sqlite-rolling): Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation. -- temporary (REASONED; sqlite-rolling): Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions. -- encryption (REASONED; see, cipher): Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption. -- encryption-keys (REASONED; see, sqlite-rolling): Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak. -- turso-http (REASONED; turso): Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git. -- turso-scope (REASONED; turso): db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens. -- replicas (REASONED; litestream): Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials. -- metrics (REASONED; litestream): Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090. -- mcp (REASONED; mcp): Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001. -- litefs-backups (REASONED; litefs-docs): LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations. -- litefs-transport (REASONED; litefs, go): LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers. -- litefs-bind (REASONED; litefs, go): http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port. -- litefs-config (REASONED; litefs): Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion. -- import (REASONED; litefs): POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status. -- export (REASONED; litefs): GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404. -- stream (REASONED; litefs): POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots. -- tx (REASONED; litefs): POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404. -- halt (REASONED; litefs): POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id. -- halt-ttl (REASONED; litefs): LiteFS halt-lock TTL defaults to 30 seconds. -- promote (REASONED; litefs): POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required. -- handoff (REASONED; litefs): POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required. -- information (REASONED; litefs): GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database. -- debug (REASONED; litefs, go): Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name. -- debug-rand (REASONED; litefs): /debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client. -- cluster-private (REASONED; litefs, litefs-docs): Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy. -- consul-url (REASONED; litefs): Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port. -- static-url (REASONED; litefs): Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API. -- container (REASONED; litefs, docker): LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability. -- proxy (REASONED; litefs): LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary. -- verify-download (REASONED; sqlite-rolling, bash, curl): Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available. -- verify-bundle (DEMONSTRATED; turso, bash, grep-docs): Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths. -- verify-git (DEMONSTRATED; sqlite-rolling): Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded. -- verify-modes (REASONED; sqlite-rolling, coreutils): SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised. -- verify-listeners (REASONED; litefs, litestream, mcp, bash, curl): Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available. -- verify-turso (REASONED; turso, bash, curl): Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available. -- halt-expiration (REASONED; litefs): LiteFS checks halt-lock expiration every 5 seconds. - +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. SQLite has no server process and no network listener: "there are no other processes, threads, machines, or other mechanisms (apart from host computer OS and filesystem) to help provide database services" ([SQLite: serverless](https://www.sqlite.org/serverless.html)), so there is no database-side authentication and the only access control is the filesystem's. Its security page warns that "any database file which might have ever been writable by an agent in a different security domain should be treated as suspect." So the deployment exposure is rarely a flaw in SQLite itself; it is wherever the database file ends up: under a web root, inside a git repository, readable by another local account, or replicated to a public bucket. Run the application with access only to the files it needs, since `ATTACH` opens further databases under the process's filesystem authority; keep extension loading disabled unless required (core SQLite disables it by default, though the SQLite CLI enables it) and load only trusted extension libraries. Application-level SQL-injection prevention is outside this guide's scope. @@ -34376,102 +36119,375 @@ Applicability checked on 2026-09-18: SQLite 3.x documentation (local file tests - GNU grep documentation v3.12 (checked October 2026): [-E selects extended regular expressions (1142-1148)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142); [-F selects fixed strings (1150-1156)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150); [ordinary regexp characters (1274-1277)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274); [{n,} repeats at least n times (1316-1320)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316); [bracket expressions match one listed character (1363-1373)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363). - curl curl-8_14_1 behaviour used by this guide's probes: the cited `-q`, `-g` and `--noproxy '*'` controls suppress curlrc, URL globbing and proxies; `--connect-timeout 5` bounds connection setup and `--max-time 20` bounds each transfer. The cited `-sS` combination hides progress while retaining errors, `-o /dev/null` suppresses bodies, and `-w` prints the response code, transfer exit code and error text even on failure. HTTP status is separate from transfer success. For Turso, the cited `--data` selects POST and `-H 'Content-Type: application/json'` replaces curl's default content type; the request payload and the required anonymous/authorized outcomes remain this guide's procedure using the Turso reference, not curl's guarantee. The cited default TLS checks support the requirement never to add `-k`. -====================================================================== -==> sqlite-http-frontends.md -====================================================================== - -# SQLite HTTP front-ends: Datasette and sqlite-web +## 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/sqlite.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): -- ds-container: Datasette container generator (caf238aac86ebe370959b25384d6e05f6a1e2359) -- ds-cloud: Datasette Cloud Run publisher (e889403d3bbe143854262682161c98a57bdb6594) -- fly: Datasette Fly publishing plugin (unknown) -- passwords: datasette-auth-passwords (1.1.1) -- github-auth: datasette-auth-github (unknown) -- write: datasette-write (0.4) -- web: sqlite-web package (0.8.1) -- web-source: sqlite-web implementation (825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2) -- web-tag-source: sqlite-web tagged implementation (0.8.1) -- web-image: sqlite-web Dockerfile (7f6658b012a789f0b9418cad543e7798ce850130) -- sqlite: SQLite documentation (unknown) -- docker: Docker localhost-publication boundary (28.0.0) -- cloudrun: Cloud Run documentation (unknown) -- nginx: nginx documentation (unknown) -- iap: Google IAP documentation (unknown) -- access: Cloudflare Access documentation (unknown) -- tailscale: Tailscale documentation (unknown) -- curl: curl minimum write-out version (7.75.0) -- ss: ss documentation (unknown) -- datasette-docs: Datasette documentation (rolling) (unknown) -- caddy-docs: Caddy documentation (rolling) (unknown) +- sqlite-rolling: SQLite documentation (rolling) (unknown) +- mcp: Litestream MCP minimum (v0.5.0) +- see: SQLite Encryption Extension (unknown) +- turso: Turso documentation (unknown) +- litestream: Litestream documentation (unknown) +- cipher: SQLCipher (unknown) +- litefs: LiteFS source (v0.5.14) +- litefs-docs: LiteFS documentation (unknown) +- go: Go library documentation (unknown) +- docker: Docker documentation (unknown) +- git: Git documentation (v2.56.0) +- coreutils: GNU Coreutils stat manual (v9.7) +- bash: GNU Bash reference manual (bash-5.3) +- curl: curl (curl-8_14_1) +- grep-docs: GNU grep documentation (v3.12) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- release (REASONED; datasette-docs): The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes. -- datasette-bind (DEMONSTRATED; datasette-docs): Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory. -- immutable (REASONED; sqlite, datasette-docs): Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors. -- datasette-docker (REASONED; datasette-docs): Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate. -- container-generator (REASONED; ds-container): At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address. -- cloudrun (REASONED; ds-cloud, cloudrun): Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization. -- publish-plugins (REASONED; fly, datasette-docs): Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established. -- docker-publication (REASONED; docker): Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally. -- actor-policy (REASONED; github-auth, datasette-docs): Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it. -- root-url (REASONED; datasette-docs): datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential. -- canned-writes (REASONED; datasette-docs): Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode. -- write-plugin (REASONED; write, datasette-docs): datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes. -- sql-policy (REASONED; datasette-docs): ?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists. -- query-limits (REASONED; datasette-docs): Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service. -- downloads (DEMONSTRATED; datasette-docs): allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403. -- cors (DEMONSTRATED; datasette-docs): The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag. -- introspection (REASONED; datasette-docs): /-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission. -- hash-urls (REASONED; datasette-docs): Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization. -- csrf (REASONED; datasette-docs): Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads. -- plugin-secrets (DEMONSTRATED; passwords, datasette-docs): The local analyst saw inline password hashes in metadata, but only $env references after environment indirection. -- web-bind (DEMONSTRATED; web, web-source): Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory. -- web-capabilities (REASONED; web): sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise. -- web-container (REASONED; web-image, web): sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy. -- web-password (DEMONSTRATED; web, web-source): Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/. -- web-readonly (DEMONSTRATED; web, web-source, sqlite): In the local --read-only run the anonymous read still returned 200 while the insert changed nothing. -- web-methods (REASONED; web-source): Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access. -- web-tls (REASONED; web): sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy. -- web-csrf (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy. -- web-cookie (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults. -- fronting (REASONED; nginx, caddy-docs, datasette-docs): Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route. -- identity-proxy (REASONED; iap, access): IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes. -- mfa (REASONED; web, access, datasette-docs): Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA. -- tailnet (REASONED; tailscale): Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured. -- origin-bypass (REASONED; iap): Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions. -- os-account (REASONED; sqlite): Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority. -- verify-inventory (REASONED; web, ss, docker, cloudrun, datasette-docs): Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability. -- verify-external (REASONED; web, iap, curl, datasette-docs): External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available. -- verify-anonymous (REASONED; web, datasette-docs): UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control. -- datasette-exposed (DEMONSTRATED; datasette-docs): Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200. -- web-exposed (DEMONSTRATED; web, web-source): Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database. -- verify-authorized (REASONED; web, curl, iap, access, datasette-docs): Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy. -- actor-results (DEMONSTRATED; passwords, datasette-docs): Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false. -- web-auth-results (DEMONSTRATED; web, web-source): Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/. -- introspection-results (DEMONSTRATED; passwords, datasette-docs): Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst. -- caddy-results (DEMONSTRATED; caddy-docs, datasette-docs): Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation. -- local-checks (DEMONSTRATED; curl): Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes. -- guard-limits (DEMONSTRATED; curl): Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage. -- datasette-cli (REASONED; datasette-docs): Bare datasette invokes serve; -h/--host and -p/--port override its bind and port. -- web-cli (REASONED; web, web-source): sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080. -- write-test (DEMONSTRATED; write, datasette-docs): Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row. -- secret-indirection (REASONED; datasette-docs): Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files. -- web-auth-config (REASONED; web, web-source, web-tag-source): In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles. -- web-readonly-config (REASONED; web, web-source, sqlite): --read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers. -- cors-policy (REASONED; datasette-docs): --cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests. -- query-pages (REASONED; datasette-docs): Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary. +- filesystem (REASONED; sqlite-rolling): No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect. +- extensions (REASONED; sqlite-rolling): Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries. +- web-files (REASONED; sqlite-rolling): Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names. +- git-files (REASONED; sqlite-rolling, git): Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit. +- permissions (REASONED; sqlite-rolling): Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation. +- temporary (REASONED; sqlite-rolling): Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions. +- encryption (REASONED; see, cipher): Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption. +- encryption-keys (REASONED; see, sqlite-rolling): Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak. +- turso-http (REASONED; turso): Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git. +- turso-scope (REASONED; turso): db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens. +- replicas (REASONED; litestream): Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials. +- metrics (REASONED; litestream): Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090. +- mcp (REASONED; mcp): Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001. +- litefs-backups (REASONED; litefs-docs): LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations. +- litefs-transport (REASONED; litefs, go): LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers. +- litefs-bind (REASONED; litefs, go): http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port. +- litefs-config (REASONED; litefs): Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion. +- import (REASONED; litefs): POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status. +- export (REASONED; litefs): GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404. +- stream (REASONED; litefs): POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots. +- tx (REASONED; litefs): POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404. +- halt (REASONED; litefs): POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id. +- halt-ttl (REASONED; litefs): LiteFS halt-lock TTL defaults to 30 seconds. +- promote (REASONED; litefs): POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required. +- handoff (REASONED; litefs): POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required. +- information (REASONED; litefs): GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database. +- debug (REASONED; litefs, go): Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name. +- debug-rand (REASONED; litefs): /debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client. +- cluster-private (REASONED; litefs, litefs-docs): Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy. +- consul-url (REASONED; litefs): Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port. +- static-url (REASONED; litefs): Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API. +- container (REASONED; litefs, docker): LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability. +- proxy (REASONED; litefs): LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary. +- verify-download (REASONED; sqlite-rolling, bash, curl): Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available. +- verify-bundle (DEMONSTRATED; turso, bash, grep-docs): Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths. +- verify-git (DEMONSTRATED; sqlite-rolling): Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded. +- verify-modes (REASONED; sqlite-rolling, coreutils): SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised. +- verify-listeners (REASONED; litefs, litestream, mcp, bash, curl): Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available. +- verify-turso (REASONED; turso, bash, curl): Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available. +- halt-expiration (REASONED; litefs): LiteFS checks halt-lock expiration every 5 seconds. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "1df2b900bb4797e8f2561a2cb2066c6c74bf8505a9043279c660e1d96229198f", + "components": { + "sqlite-rolling": { + "name": "SQLite documentation (rolling)", + "basis": "unknown", + "sources": { + "s2721da91f8d2": "https://www.sqlite.org/serverless.html", + "sf3d7f8391994": "https://www.sqlite.org/tempfiles.html", + "s742deb3338bb": "https://sqlite.org/loadext.html", + "sadf0431c1271": "https://www.sqlite.org/security.html" + } + }, + "mcp": { + "name": "Litestream MCP minimum", + "basis": "v0.5.0", + "sources": { + "sa5dddf04652c": "https://litestream.io/reference/config/" + } + }, + "see": { + "name": "SQLite Encryption Extension", + "basis": "unknown", + "sources": { + "sda3d8876503d": "https://www.sqlite.org/see/doc/trunk/www/readme.wiki" + } + }, + "turso": { + "name": "Turso documentation", + "basis": "unknown", + "sources": { + "sb32fd9c679e1": "https://docs.turso.tech/sdk/http/quickstart", + "sfec4e734f423": "https://docs.turso.tech/cli/db/tokens/create", + "se409838a474a": "https://docs.turso.tech/sdk/http/reference" + } + }, + "litestream": { + "name": "Litestream documentation", + "basis": "unknown", + "sources": { + "sec3d72af1447": "https://litestream.io/guides/", + "s87fa51accafc": "https://litestream.io/guides/s3/", + "sa5dddf04652c": "https://litestream.io/reference/config/" + } + }, + "cipher": { + "name": "SQLCipher", + "basis": "unknown", + "sources": { + "s57d83a42a74a": "https://www.zetetic.net/sqlcipher/" + } + }, + "litefs": { + "name": "LiteFS source", + "basis": "v0.5.14", + "sources": { + "s694a6ba8eafa": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L134-L268", + "s864cacb8e7cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L76-L99", + "s16da1761d6cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/client.go#L32-L43", + "sf0e982d0e36a": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L57", + "s9389d639da6b": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L54-L58", + "s475c28fd949e": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L76-L110", + "s055e15e84ba5": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L219-L233", + "s0b0bac964067": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L288-L333", + "s326a1007e5c2": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L294-L318", + "s4547cc32557d": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2779-L2811", + "s85ece0e83d43": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1904-L1910", + "sa467eeb47f34": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L320-L346", + "s5ffa5c20fc78": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2682-L2776", + "s02908966756d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L495-L520", + "sd63fa776fbc1": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L526-L585", + "s1c51b7efc42d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L686-L699", + "scd0168363c1a": "https://github.com/superfly/litefs/blob/v0.5.14/http/http.go#L15-L43", + "s298808a10d50": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L461-L493", + "s7269cca8b0ea": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2387-L2448", + "sbd59a1af784e": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2452-L2609", + "sa199174f541b": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L348-L407", + "s750403129022": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L215-L325", + "s5009045b97e1": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1525-L1531", + "se2add54cf935": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L39-L41", + "s59a2dfccd853": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1455-L1465", + "s246e6ad0d330": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L409-L459", + "sae4ece3fbc87": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L408-L432", + "s7bcb006d6884": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L168-L170", + "se1a41376a98d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L271-L292", + "s93c0ad91a567": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L779-L803", + "s36792457dd64": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L487-L488", + "s7234e34af9ef": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1661-L1713", + "s4cebe21e711d": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L329-L348", + "sb43f5d78723c": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L241-L244", + "s8dfa3f10f3d3": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L99-L131", + "s83d8bd834347": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1384-L1387", + "s3284d1a3ecad": "https://github.com/superfly/litefs/blob/v0.5.14/Dockerfile", + "s2b80fdf8adbe": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L519-L524", + "sc0568fe7e0ee": "https://github.com/superfly/litefs/blob/v0.5.14/http/proxy_server.go#L131-L144", + "sf0d31f900082": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L60-L68", + "s82103ee03d64": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L32-L35" + } + }, + "litefs-docs": { + "name": "LiteFS documentation", + "basis": "unknown", + "sources": { + "s13b9e92d9996": "https://fly.io/docs/litefs/", + "sd7c0ba136b25": "https://fly.io/docs/litefs/config/" + } + }, + "go": { + "name": "Go library documentation", + "basis": "unknown", + "sources": { + "s6c5be8454523": "https://pkg.go.dev/github.com/golang/net/http2/h2c#NewHandler", + "s37204ff1b27c": "https://pkg.go.dev/net#Listen", + "se49529035a0c": "https://pkg.go.dev/expvar", + "s0246034dddd1": "https://pkg.go.dev/net/http/pprof" + } + }, + "docker": { + "name": "Docker documentation", + "basis": "unknown", + "sources": { + "s881082f899b2": "https://docs.docker.com/engine/network/drivers/bridge/", + "s6732393fd414": "https://docs.docker.com/engine/network/drivers/host/", + "s00095bd02553": "https://docs.docker.com/engine/network/#container-networks" + } + }, + "git": { + "name": "Git documentation", + "basis": "v2.56.0", + "sources": { + "s12aad6467212": "https://github.com/git/git/blob/v2.56.0/Documentation/gitignore.adoc#L15-L18", + "s8054ab09dc74": "https://github.com/git/git/blob/v2.56.0/Documentation/git-rm.adoc#L17-L28", + "s9a6802adec89": "https://github.com/git/git/blob/v2.56.0/Documentation/git-log.adoc#L16-L16" + } + }, + "coreutils": { + "name": "GNU Coreutils stat manual", + "basis": "v9.7", + "sources": { + "s247df4545dc0": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L12989-L12995", + "sfd49553ab6e5": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13046", + "sbd6f7cad7bf4": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13062", + "s3e55a7242c33": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13073" + } + }, + "bash": { + "name": "GNU Bash reference manual", + "basis": "bash-5.3", + "sources": { + "s3711969be580": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n450", + "s7fec9b923488": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n769", + "s77b19e384d70": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1397", + "se4f37e01a249": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1866", + "s07f77f16f9f5": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2288", + "sbed8cd562a35": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2993", + "s7c9e44003867": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3131", + "s223b5c4d9a28": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4350", + "se02ece9163d4": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4582", + "s6067b2b1f78a": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5216", + "sa506f1abd6a1": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6182", + "s7d29125b4005": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n8232", + "s8962e63cfb20": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2580", + "s8a27e686ddfb": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5466", + "s213f6d8d1f07": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3280", + "sf9757e9fd6e8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3872", + "s8cbc1b4709e2": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4737", + "s3255eca7e496": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4852", + "s252aa2e81179": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5156", + "s8447036b2114": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5161", + "s5cab49305f3d": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5548", + "s6e8ae812c1c3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5625", + "sb38bf74a89b0": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5629", + "s260459961fbc": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5659", + "s8c59508bd4c8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5917", + "s8016d1aecfca": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5929", + "se62fd3ad4fc3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6122", + "s2df39d2c9c1c": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6194", + "s80e3246e96d8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n7274" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5b71321b79ea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/disable.md#L4-L20", + "sc405a4bffc27": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/globoff.md#L4-L22", + "s345ec65025b0": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/noproxy.md#L18-L28", + "sc9973c4ceec1": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/connect-timeout.md#L19-L28", + "s7b6dca376156": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/max-time.md#L21-L27", + "s156481f6b726": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/silent.md#L4-L25", + "s07916104bf60": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/show-error.md#L4-L19", + "sa937dbe5dd4a": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L4-L33", + "s021603e35e68": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L49-L53", + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s39402f9cb25c": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L105-L107", + "sf2dcd83157a6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/insecure.md#L4-L30", + "s13f463154cea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/fail.md#L24-L28", + "s2f1ff2f6cacd": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/output.md#L4-L53", + "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" + } + }, + "grep-docs": { + "name": "GNU grep documentation", + "basis": "v3.12", + "sources": { + "s1733b640052b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197", + "s99f121f5426a": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342", + "sbc8d05ab659b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621", + "sfdd7a73eab39": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757", + "s7f9dc35e81c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788", + "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", + "sc08d6af4d642": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142", + "scd528c854b46": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150", + "sb8be8b716426": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274", + "s5d5af9297df2": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316", + "se99a83dba3c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363" + } + } + }, + "claims": { + "filesystem": {"text": "No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sadf0431c1271"], "status": "REASONED"}, + "extensions": {"text": "Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s742deb3338bb"], "status": "REASONED"}, + "web-files": {"text": "Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "git-files": {"text": "Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit.", "components": ["sqlite-rolling", "git"], "sources": ["sqlite-rolling:sf3d7f8391994", "git:s12aad6467212", "git:s8054ab09dc74", "git:s9a6802adec89"], "status": "REASONED"}, + "permissions": {"text": "Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "temporary": {"text": "Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "encryption": {"text": "Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption.", "components": ["see", "cipher"], "sources": ["see:sda3d8876503d", "cipher:s57d83a42a74a"], "status": "REASONED"}, + "encryption-keys": {"text": "Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak.", "components": ["see", "sqlite-rolling"], "sources": ["see:sda3d8876503d", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "turso-http": {"text": "Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git.", "components": ["turso"], "sources": ["turso:sb32fd9c679e1"], "status": "REASONED"}, + "turso-scope": {"text": "db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens.", "components": ["turso"], "sources": ["turso:sfec4e734f423"], "status": "REASONED"}, + "replicas": {"text": "Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials.", "components": ["litestream"], "sources": ["litestream:sec3d72af1447", "litestream:s87fa51accafc"], "status": "REASONED"}, + "metrics": {"text": "Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090.", "components": ["litestream"], "sources": ["litestream:sa5dddf04652c"], "status": "REASONED"}, + "mcp": {"text": "Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001.", "components": ["mcp"], "sources": ["mcp:sa5dddf04652c"], "status": "REASONED"}, + "litefs-backups": {"text": "LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations.", "components": ["litefs-docs"], "sources": ["litefs-docs:s13b9e92d9996"], "status": "REASONED"}, + "litefs-transport": {"text": "LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc", "litefs:s16da1761d6cc", "go:s6c5be8454523"], "status": "REASONED"}, + "litefs-bind": {"text": "http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port.", "components": ["litefs", "go"], "sources": ["litefs:sf0e982d0e36a", "litefs:s9389d639da6b", "go:s37204ff1b27c", "litefs:s82103ee03d64"], "status": "REASONED"}, + "litefs-config": {"text": "Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion.", "components": ["litefs"], "sources": ["litefs:s475c28fd949e", "litefs:s055e15e84ba5", "litefs:s0b0bac964067"], "status": "REASONED"}, + "import": {"text": "POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status.", "components": ["litefs"], "sources": ["litefs:s326a1007e5c2", "litefs:s4547cc32557d", "litefs:s85ece0e83d43"], "status": "REASONED"}, + "export": {"text": "GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404.", "components": ["litefs"], "sources": ["litefs:sa467eeb47f34", "litefs:s5ffa5c20fc78"], "status": "REASONED"}, + "stream": {"text": "POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots.", "components": ["litefs"], "sources": ["litefs:s02908966756d", "litefs:sd63fa776fbc1", "litefs:s1c51b7efc42d", "litefs:scd0168363c1a"], "status": "REASONED"}, + "tx": {"text": "POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404.", "components": ["litefs"], "sources": ["litefs:s298808a10d50", "litefs:s7269cca8b0ea", "litefs:sbd59a1af784e"], "status": "REASONED"}, + "halt": {"text": "POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id.", "components": ["litefs"], "sources": ["litefs:sa199174f541b", "litefs:s750403129022", "litefs:s5009045b97e1"], "status": "REASONED"}, + "halt-ttl": {"text": "LiteFS halt-lock TTL defaults to 30 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"}, + "promote": {"text": "POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:sae4ece3fbc87"], "status": "REASONED"}, + "handoff": {"text": "POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:s7bcb006d6884"], "status": "REASONED"}, + "information": {"text": "GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:se1a41376a98d", "litefs:s93c0ad91a567"], "status": "REASONED"}, + "debug": {"text": "Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s36792457dd64", "litefs:s7234e34af9ef", "go:se49529035a0c", "go:s0246034dddd1"], "status": "REASONED"}, + "debug-rand": {"text": "/debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc"], "status": "REASONED"}, + "cluster-private": {"text": "Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy.", "components": ["litefs", "litefs-docs"], "sources": ["litefs:s694a6ba8eafa", "litefs-docs:sd7c0ba136b25"], "status": "REASONED"}, + "consul-url": {"text": "Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port.", "components": ["litefs"], "sources": ["litefs:s4cebe21e711d"], "status": "REASONED"}, + "static-url": {"text": "Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API.", "components": ["litefs"], "sources": ["litefs:s8dfa3f10f3d3", "litefs:s83d8bd834347", "litefs:sb43f5d78723c"], "status": "REASONED"}, + "container": {"text": "LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability.", "components": ["litefs", "docker"], "sources": ["litefs:s3284d1a3ecad", "docker:s881082f899b2", "docker:s6732393fd414", "docker:s00095bd02553"], "status": "REASONED"}, + "proxy": {"text": "LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary.", "components": ["litefs"], "sources": ["litefs:s2b80fdf8adbe", "litefs:sc0568fe7e0ee", "litefs:sf0d31f900082"], "status": "REASONED"}, + "verify-download": {"text": "Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available.", "components": ["sqlite-rolling", "bash", "curl"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [1]}, + "verify-bundle": {"text": "Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths.", "components": ["turso", "bash", "grep-docs"], "sources": ["turso:sb32fd9c679e1", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "grep-docs:s1733b640052b", "grep-docs:s99f121f5426a", "grep-docs:sbc8d05ab659b", "grep-docs:sfdd7a73eab39", "grep-docs:s7f9dc35e81c5", "grep-docs:s5170317ddea8", "grep-docs:sc08d6af4d642", "grep-docs:scd528c854b46", "grep-docs:sb8be8b716426", "grep-docs:s5d5af9297df2", "grep-docs:se99a83dba3c5"], "status": "DEMONSTRATED", "evidence": "A bundle carrying the placeholder printed `dist/app.js` before `token-literal exit: 0` and again before `jwt-shape exit: 0`, and a clean bundle printed `1` for both.", "verify": [2]}, + "verify-git": {"text": "Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "DEMONSTRATED", "evidence": "With `app.db` committed before an `app.db*` ignore rule was added, `git check-ignore -v` listed the sidecars but not the tracked `app.db`. `git ls-files` printed `app.db`, and `git log` printed its commit.", "verify": [3]}, + "verify-modes": {"text": "SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised.", "components": ["sqlite-rolling", "coreutils"], "sources": ["sqlite-rolling:sf3d7f8391994", "coreutils:s247df4545dc0", "coreutils:sfd49553ab6e5", "coreutils:sbd6f7cad7bf4", "coreutils:s3e55a7242c33"], "status": "REASONED", "verify": [4]}, + "verify-listeners": {"text": "Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available.", "components": ["litefs", "litestream", "mcp", "bash", "curl"], "sources": ["litefs:s694a6ba8eafa", "litestream:sa5dddf04652c", "mcp:sa5dddf04652c", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [5]}, + "verify-turso": {"text": "Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available.", "components": ["turso", "bash", "curl"], "sources": ["turso:se409838a474a", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:sd3b039bafbf6", "curl:s3db522258e44"], "status": "REASONED", "verify": [6]}, + "halt-expiration": {"text": "LiteFS checks halt-lock expiration every 5 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"} + } +} +```` +====================================================================== +==> sqlite-http-frontends.md +====================================================================== + +# SQLite HTTP front-ends: Datasette and sqlite-web + +Written against Datasette container generator (pinned to a commit), sqlite-web package 0.8.1, sqlite-web tagged implementation 0.8.1, datasette-auth-passwords 1.1.1, datasette-write 0.4, Docker localhost-publication boundary 28.0.0, and curl minimum write-out version 7.75.0; 3 further sources are pinned to a commit and 11 carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 55 claims: 18 demonstrated by a check that ran and 37 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. + An SQLite HTTP front-end turns a local file into a network service: **the port is the exposure**. Datasette allows anonymous browsing and read-only SQL by default; sqlite-web provides queries, imports, exports, row changes, and schema deletion. An unprotected, reachable instance exposes those capabilities. See Datasette permissions and sqlite-web features. A reachable, unauthenticated, writable sqlite-web instance therefore permits compromise of the databases its process can modify. That is an inference from its documented capabilities, not a claim of automatic host compromise. @@ -34747,6 +36763,358 @@ Every URL listed below was fetched during authoring, except the sqlite-web 0.8.1 - ss manual: https://man7.org/linux/man-pages/man8/ss.8.html - OWASP SQL injection prevention: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.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/sqlite-http-frontends.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): + +- ds-container: Datasette container generator (caf238aac86ebe370959b25384d6e05f6a1e2359) +- web: sqlite-web package (0.8.1) +- web-tag-source: sqlite-web tagged implementation (0.8.1) +- ds-cloud: Datasette Cloud Run publisher (e889403d3bbe143854262682161c98a57bdb6594) +- fly: Datasette Fly publishing plugin (unknown) +- datasette-docs: Datasette documentation (rolling) (unknown) +- web-source: sqlite-web implementation (825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2) +- web-image: sqlite-web Dockerfile (7f6658b012a789f0b9418cad543e7798ce850130) +- passwords: datasette-auth-passwords (1.1.1) +- github-auth: datasette-auth-github (unknown) +- write: datasette-write (0.4) +- sqlite: SQLite documentation (unknown) +- docker: Docker localhost-publication boundary (28.0.0) +- cloudrun: Cloud Run documentation (unknown) +- nginx: nginx documentation (unknown) +- iap: Google IAP documentation (unknown) +- access: Cloudflare Access documentation (unknown) +- tailscale: Tailscale documentation (unknown) +- caddy-docs: Caddy documentation (rolling) (unknown) +- curl: curl minimum write-out version (7.75.0) +- ss: ss documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- release (REASONED; datasette-docs): The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes. +- datasette-bind (DEMONSTRATED; datasette-docs): Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory. +- immutable (REASONED; sqlite, datasette-docs): Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors. +- datasette-docker (REASONED; datasette-docs): Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate. +- container-generator (REASONED; ds-container): At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address. +- cloudrun (REASONED; ds-cloud, cloudrun): Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization. +- publish-plugins (REASONED; fly, datasette-docs): Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established. +- docker-publication (REASONED; docker): Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally. +- actor-policy (REASONED; github-auth, datasette-docs): Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it. +- root-url (REASONED; datasette-docs): datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential. +- canned-writes (REASONED; datasette-docs): Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode. +- write-plugin (REASONED; write, datasette-docs): datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes. +- sql-policy (REASONED; datasette-docs): ?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists. +- query-limits (REASONED; datasette-docs): Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service. +- downloads (DEMONSTRATED; datasette-docs): allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403. +- cors (DEMONSTRATED; datasette-docs): The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag. +- introspection (REASONED; datasette-docs): /-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission. +- hash-urls (REASONED; datasette-docs): Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization. +- csrf (REASONED; datasette-docs): Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads. +- plugin-secrets (DEMONSTRATED; passwords, datasette-docs): The local analyst saw inline password hashes in metadata, but only $env references after environment indirection. +- web-bind (DEMONSTRATED; web, web-source): Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory. +- web-capabilities (REASONED; web): sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise. +- web-container (REASONED; web-image, web): sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy. +- web-password (DEMONSTRATED; web, web-source): Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/. +- web-readonly (DEMONSTRATED; web, web-source, sqlite): In the local --read-only run the anonymous read still returned 200 while the insert changed nothing. +- web-methods (REASONED; web-source): Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access. +- web-tls (REASONED; web): sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy. +- web-csrf (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy. +- web-cookie (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults. +- fronting (REASONED; nginx, caddy-docs, datasette-docs): Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route. +- identity-proxy (REASONED; iap, access): IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes. +- mfa (REASONED; web, access, datasette-docs): Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA. +- tailnet (REASONED; tailscale): Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured. +- origin-bypass (REASONED; iap): Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions. +- os-account (REASONED; sqlite): Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority. +- verify-inventory (REASONED; web, ss, docker, cloudrun, datasette-docs): Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability. +- verify-external (REASONED; web, iap, curl, datasette-docs): External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available. +- verify-anonymous (REASONED; web, datasette-docs): UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control. +- datasette-exposed (DEMONSTRATED; datasette-docs): Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200. +- web-exposed (DEMONSTRATED; web, web-source): Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database. +- verify-authorized (REASONED; web, curl, iap, access, datasette-docs): Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy. +- actor-results (DEMONSTRATED; passwords, datasette-docs): Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false. +- web-auth-results (DEMONSTRATED; web, web-source): Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/. +- introspection-results (DEMONSTRATED; passwords, datasette-docs): Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst. +- caddy-results (DEMONSTRATED; caddy-docs, datasette-docs): Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation. +- local-checks (DEMONSTRATED; curl): Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes. +- guard-limits (DEMONSTRATED; curl): Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage. +- datasette-cli (REASONED; datasette-docs): Bare datasette invokes serve; -h/--host and -p/--port override its bind and port. +- web-cli (REASONED; web, web-source): sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080. +- write-test (DEMONSTRATED; write, datasette-docs): Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row. +- secret-indirection (REASONED; datasette-docs): Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files. +- web-auth-config (REASONED; web, web-source, web-tag-source): In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles. +- web-readonly-config (REASONED; web, web-source, sqlite): --read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers. +- cors-policy (REASONED; datasette-docs): --cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests. +- query-pages (REASONED; datasette-docs): Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "cd2fba66ff332b791a1934a25ece6c5ef7f76fc791a8a4019819189f360eab0b", + "components": { + "ds-container": { + "name": "Datasette container generator", + "basis": "caf238aac86ebe370959b25384d6e05f6a1e2359", + "sources": { + "s3c9be24424e0": "https://raw.githubusercontent.com/simonw/datasette/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py", + "sffebbc2c5528": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L576-L655", + "seae12396d87a": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L658-L736", + "s9e735f75bf4b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L173-L179", + "s224af1264bb1": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L203-L306", + "sf0ea47721a46": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L352-L358", + "sae5997e91b6b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/cloudrun.py#L17-L244", + "s735b0ff17b61": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/heroku.py#L21-L254", + "s2484732fbf31": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/tests/test_package.py#L11-L61" + } + }, + "web": { + "name": "sqlite-web package", + "basis": "0.8.1", + "sources": { + "s91f8591a5f84": "https://github.com/coleifer/sqlite-web/blob/0.8.1/README.md" + } + }, + "web-tag-source": { + "name": "sqlite-web tagged implementation", + "basis": "0.8.1", + "sources": { + "s1626cd0d975f": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L3", + "s8c12f9aab57c": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1735-L1740", + "s30bba3ce63c6": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1944-L1956" + } + }, + "ds-cloud": { + "name": "Datasette Cloud Run publisher", + "basis": "e889403d3bbe143854262682161c98a57bdb6594", + "sources": { + "sf5859249ea9f": "https://github.com/simonw/datasette/blob/e889403d3bbe143854262682161c98a57bdb6594/datasette/publish/cloudrun.py" + } + }, + "fly": { + "name": "Datasette Fly publishing plugin", + "basis": "unknown", + "sources": { + "s2a2db4192225": "https://github.com/simonw/datasette-publish-fly" + } + }, + "datasette-docs": { + "name": "Datasette documentation (rolling)", + "basis": "unknown", + "sources": { + "s3e64933fe2e9": "https://docs.datasette.io/en/stable/changelog.html", + "sb0aae2446988": "https://docs.datasette.io/en/stable/cli-reference.html", + "sca59b58c5c84": "https://docs.datasette.io/en/stable/performance.html", + "s8f2fe7a13b3f": "https://docs.datasette.io/en/stable/installation.html", + "se7cc29f79be8": "https://docs.datasette.io/en/stable/publish.html", + "s878851fd8eac": "https://docs.datasette.io/en/stable/authentication.html", + "s824eb176fda7": "https://docs.datasette.io/en/stable/sql_queries.html", + "s406cc998a5da": "https://docs.datasette.io/en/stable/internals.html#csrf-protection", + "se154b0fa47dd": "https://docs.datasette.io/en/stable/settings.html", + "s96c8e6562139": "https://docs.datasette.io/en/stable/json_api.html", + "s1e93b280c5af": "https://docs.datasette.io/en/stable/introspection.html", + "sb1b30f98f026": "https://docs.datasette.io/en/stable/plugins.html#secret-configuration-values", + "s53e84082f6d1": "https://docs.datasette.io/en/stable/deploying.html" + } + }, + "web-source": { + "name": "sqlite-web implementation", + "basis": "825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2", + "sources": { + "s13d71c0a8cb2": "https://raw.githubusercontent.com/coleifer/sqlite-web/825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2/sqlite_web/sqlite_web.py" + } + }, + "web-image": { + "name": "sqlite-web Dockerfile", + "basis": "7f6658b012a789f0b9418cad543e7798ce850130", + "sources": { + "s4c81e6becc20": "https://raw.githubusercontent.com/coleifer/sqlite-web/7f6658b012a789f0b9418cad543e7798ce850130/docker/Dockerfile" + } + }, + "passwords": { + "name": "datasette-auth-passwords", + "basis": "1.1.1", + "sources": { + "sb6597be5af67": "https://github.com/simonw/datasette-auth-passwords/blob/1.1.1/README.md" + } + }, + "github-auth": { + "name": "datasette-auth-github", + "basis": "unknown", + "sources": { + "s90107b903580": "https://github.com/simonw/datasette-auth-github" + } + }, + "write": { + "name": "datasette-write", + "basis": "0.4", + "sources": { + "s073e10d21091": "https://github.com/simonw/datasette-write/blob/0.4/README.md" + } + }, + "sqlite": { + "name": "SQLite documentation", + "basis": "unknown", + "sources": { + "s4e4e320abf5b": "https://www.sqlite.org/uri.html", + "s2721da91f8d2": "https://www.sqlite.org/serverless.html", + "sadf0431c1271": "https://www.sqlite.org/security.html" + } + }, + "docker": { + "name": "Docker localhost-publication boundary", + "basis": "28.0.0", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "cloudrun": { + "name": "Cloud Run documentation", + "basis": "unknown", + "sources": { + "sb90a6c13388a": "https://docs.cloud.google.com/run/docs/container-contract" + } + }, + "nginx": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", + "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html" + } + }, + "iap": { + "name": "Google IAP documentation", + "basis": "unknown", + "sources": { + "saafdfc163fce": "https://docs.cloud.google.com/iap/docs/concepts-overview" + } + }, + "access": { + "name": "Cloudflare Access documentation", + "basis": "unknown", + "sources": { + "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/", + "s06747cf7388c": "https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/independent-mfa/" + } + }, + "tailscale": { + "name": "Tailscale documentation", + "basis": "unknown", + "sources": { + "s5b9a9b15ae80": "https://tailscale.com/docs/reference/tailscale-cli/serve" + } + }, + "caddy-docs": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "ss": { + "name": "ss documentation", + "basis": "unknown", + "sources": { + "sfc58f455acfa": "https://man7.org/linux/man-pages/man8/ss.8.html" + } + } + }, + "claims": { + "release": {"text": "The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9"], "status": "REASONED"}, + "datasette-bind": {"text": "Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "DEMONSTRATED", "evidence": "bare `datasette serve` with no host or port flag listened on `127.0.0.1:8001`"}, + "immutable": {"text": "Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors.", "components": ["sqlite", "datasette-docs"], "sources": ["datasette-docs:sca59b58c5c84", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, + "datasette-docker": {"text": "Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate.", "components": ["datasette-docs"], "sources": ["datasette-docs:s8f2fe7a13b3f"], "status": "REASONED"}, + "container-generator": {"text": "At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address.", "components": ["ds-container"], "sources": ["ds-container:s3c9be24424e0", "ds-container:sffebbc2c5528", "ds-container:seae12396d87a", "ds-container:s9e735f75bf4b", "ds-container:s224af1264bb1", "ds-container:sf0ea47721a46", "ds-container:sae5997e91b6b", "ds-container:s735b0ff17b61", "ds-container:s2484732fbf31"], "status": "REASONED"}, + "cloudrun": {"text": "Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization.", "components": ["ds-cloud", "cloudrun"], "sources": ["ds-cloud:sf5859249ea9f", "cloudrun:sb90a6c13388a"], "status": "REASONED"}, + "publish-plugins": {"text": "Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established.", "components": ["fly", "datasette-docs"], "sources": ["datasette-docs:se7cc29f79be8", "fly:s2a2db4192225"], "status": "REASONED"}, + "docker-publication": {"text": "Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally.", "components": ["docker"], "sources": ["docker:s1e53417c513d"], "status": "REASONED"}, + "actor-policy": {"text": "Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it.", "components": ["github-auth", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "github-auth:s90107b903580"], "status": "REASONED"}, + "root-url": {"text": "datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:sb0aae2446988"], "status": "REASONED"}, + "canned-writes": {"text": "Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode.", "components": ["datasette-docs"], "sources": ["datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "write-plugin": {"text": "datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "REASONED"}, + "sql-policy": {"text": "?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "query-limits": {"text": "Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "downloads": {"text": "allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd"], "status": "DEMONSTRATED", "evidence": "`/published.db` returned `403`; without `--setting allow_download off` it returned `200`."}, + "cors": {"text": "The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "DEMONSTRATED", "evidence": "`--cors` added `Access-Control-Allow-Origin: *`, absent without it."}, + "introspection": {"text": "/-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission.", "components": ["datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, + "hash-urls": {"text": "Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9", "datasette-docs:sca59b58c5c84"], "status": "REASONED"}, + "csrf": {"text": "Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads.", "components": ["datasette-docs"], "sources": ["datasette-docs:s406cc998a5da"], "status": "REASONED"}, + "plugin-secrets": {"text": "The local analyst saw inline password hashes in metadata, but only $env references after environment indirection.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "`/-/metadata.json` showed that analyst both users' password hashes when they were written inline in the plugin configuration, but only the `$env` references when they were supplied through environment variables."}, + "web-bind": {"text": "Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "bare `sqlite_web` on `127.0.0.1:8080`, as `ss -tlnp` run without `sudo` by the same account showed"}, + "web-capabilities": {"text": "sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, + "web-container": {"text": "sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy.", "components": ["web-image", "web"], "sources": ["web-image:s4c81e6becc20", "web:s91f8591a5f84"], "status": "REASONED"}, + "web-password": {"text": "Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "`SQLITE_WEB_PASSWORD` set without `-P` left sqlite-web open (`200` anonymously), as the source reading above says; with `-P` the anonymous query GET got `302` to `/login/`."}, + "web-readonly": {"text": "In the local --read-only run the anonymous read still returned 200 while the insert changed nothing.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "DEMONSTRATED", "evidence": "with `--read-only` the read still returned `200` and the insert changed nothing."}, + "web-methods": {"text": "Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access.", "components": ["web-source"], "sources": ["web-source:s13d71c0a8cb2"], "status": "REASONED"}, + "web-tls": {"text": "sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, + "web-csrf": {"text": "sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "an authenticated `POST` insert with no token and a foreign `Origin` header wrote a row, and the insert form contains no CSRF field."}, + "web-cookie": {"text": "sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Its session cookie was set with `HttpOnly; Path=/` and no `SameSite` or `Secure` attribute, even over TLS"}, + "fronting": {"text": "Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route.", "components": ["nginx", "caddy-docs", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "nginx:s6a0728ca7609", "nginx:sf32ce2956917", "caddy-docs:sbc2bd9599bd1"], "status": "REASONED"}, + "identity-proxy": {"text": "IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes.", "components": ["iap", "access"], "sources": ["iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED"}, + "mfa": {"text": "Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA.", "components": ["web", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "access:s06747cf7388c"], "status": "REASONED"}, + "tailnet": {"text": "Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured.", "components": ["tailscale"], "sources": ["tailscale:s5b9a9b15ae80"], "status": "REASONED"}, + "origin-bypass": {"text": "Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions.", "components": ["iap"], "sources": ["iap:saafdfc163fce"], "status": "REASONED"}, + "os-account": {"text": "Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority.", "components": ["sqlite"], "sources": ["sqlite:s2721da91f8d2", "sqlite:sadf0431c1271"], "status": "REASONED"}, + "verify-inventory": {"text": "Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability.", "components": ["web", "ss", "docker", "cloudrun", "datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "web:s91f8591a5f84", "ss:sfc58f455acfa", "docker:s1e53417c513d", "cloudrun:sb90a6c13388a"], "status": "REASONED", "verify": [1]}, + "verify-external": {"text": "External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available.", "components": ["web", "iap", "curl", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "web:s91f8591a5f84", "iap:saafdfc163fce", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, + "verify-anonymous": {"text": "UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control.", "components": ["web", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:s824eb176fda7", "web:s91f8591a5f84"], "status": "REASONED", "verify": [2]}, + "datasette-exposed": {"text": "Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "datasette-docs:se154b0fa47dd", "datasette-docs:s96c8e6562139", "datasette-docs:s1e93b280c5af"], "status": "DEMONSTRATED", "evidence": "Against the baseline command above, the SQL JSON route returned `403` while the table's JSON page still returned `200` (SQL disabled, browsing permitted)"}, + "web-exposed": {"text": "Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Bare `sqlite_web` answered the query GET with `200` anonymously, and an anonymous `POST` insert wrote a row to a disposable database"}, + "verify-authorized": {"text": "Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy.", "components": ["web", "curl", "iap", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "curl:s2b2686afaf41", "iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED", "verify": [3]}, + "actor-results": {"text": "Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "the block returned the table's rows to the `analyst` session cookie (`authorized http=200`) and `403` anonymously; an authenticated `guest` got `403`, the unauthorized outcome, and the analyst got `403` on SQL under `allow_sql: false`."}, + "web-auth-results": {"text": "Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "For sqlite-web with `-P`, the session cookie got the row with `200` and the anonymous request `302` to `/login/`."}, + "introspection-results": {"text": "Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "Under the same metadata, `/-/versions.json`, `/-/metadata.json`, `/-/plugins.json`, `/-/settings.json` and `/-/databases.json` returned `403` anonymously and `200` to a freshly logged-in analyst"}, + "caddy-results": {"text": "Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation.", "components": ["caddy-docs", "datasette-docs"], "sources": ["caddy-docs:sbc2bd9599bd1", "datasette-docs:s53e84082f6d1"], "status": "DEMONSTRATED", "evidence": "Block C through the proxy gave `authorized http=200` with the rows and `anonymous http=401` with `www-authenticate: Basic realm=\"restricted\"`."}, + "local-checks": {"text": "Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Local authoring checks passed: `bash -n` on all five shell blocks, parsing of the metadata JSON, and 120 guard executions covering 40 cases under ordinary Bash, `set -u`, and `set -u` with `IFS=0`."}, + "guard-limits": {"text": "Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Across 48 block B/C cases under ordinary Bash, set -u, and set -u with IFS=0, an explicitly substituted curl argument recorder ran with valid whole-block values, inherited matching marker/arguments, and pastes below the guards. Whole-block placeholders and omitted assignments without a matching marker exited 2 without calling it."}, + "datasette-cli": {"text": "Bare datasette invokes serve; -h/--host and -p/--port override its bind and port.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "REASONED"}, + "web-cli": {"text": "sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "REASONED"}, + "write-test": {"text": "Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "DEMONSTRATED", "evidence": "With datasette-write on a disposable database, an anonymous write got `403`, a root write without the CSRF token `403`, and with the token `302`, and the row was written."}, + "secret-indirection": {"text": "Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, + "web-auth-config": {"text": "In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles.", "components": ["web", "web-source", "web-tag-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "web-tag-source:s1626cd0d975f", "web-tag-source:s8c12f9aab57c", "web-tag-source:s30bba3ce63c6"], "status": "REASONED"}, + "web-readonly-config": {"text": "--read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, + "cors-policy": {"text": "--cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "REASONED"}, + "query-pages": {"text": "Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"} + } +} +```` + + ====================================================================== ==> surrealdb.md ====================================================================== @@ -43925,62 +46293,9 @@ Claims (full source mapping in the JSON block below): # MLflow tracking server: no authentication by default - -**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. +Written against MLflow pinned source v3.16.1, MLflow security middleware minimum 3.5.0, Docker Engine loopback publication minimum 28.0.0, nginx proxy implementation release-1.28.0, Caddy reverse proxy parser v2.11.4, oauth2-proxy documentation v7.15.4, and Authelia source v4.39.28, plus 2 more recorded bases; 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 32 claims, all reasoned from cited sources. -Components (recorded basis): - -- docs: MLflow documentation (unknown) -- source: MLflow pinned source (v3.16.1) -- middleware-min: MLflow security middleware minimum (3.5.0) -- docker: Docker Engine loopback publication minimum (28.0.0) -- curl: curl minimum write-out version (7.75.0) -- nginx: nginx proxy implementation (release-1.28.0) -- caddy: Caddy reverse proxy parser (v2.11.4) -- iproute2: iproute2 ss manual (v6.12.0) -- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) -- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) -- authelia-source: Authelia source (v4.39.28) - -Claims (full source mapping in the front matter): - -- default-host (REASONED; source): v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting. -- default-port (REASONED; source): v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it. -- default-auth (REASONED; docs): Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts. -- container-bind (REASONED; docker, docs): Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports. -- middleware-scope (REASONED; middleware-min, docs): Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it. -- host-allowlist (REASONED; docs): Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules. -- cors (REASONED; docs): Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin. -- middleware-disable (REASONED; docs): Do not use --disable-security-middleware outside a test. -- tls (REASONED; docs, nginx, caddy): There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname. -- client-tls (REASONED; docs): Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production. -- auth-app (REASONED; docs): Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label. -- csrf-key (REASONED; docs): Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control. -- env-file (REASONED; docs, source): Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching. -- key-exposure (REASONED; source, docs): Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure. -- admin-bootstrap (REASONED; docs): No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply. -- default-permission (REASONED; docs): default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant. -- auth-database (REASONED; docs): database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example. -- auth-config (REASONED; docs): MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic. -- password-rotation (REASONED; docs, curl): PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping. -- user-creation (REASONED; docs): User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user. -- client-credentials (REASONED; docs): ~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD. -- rate-limit (REASONED; docs): The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS. -- mfa (REASONED; docs, cf-docs, oauth2-proxy-source, authelia-source): MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination. -- proxy-token (REASONED; docs, source, cf-docs): MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together. -- artifact-proxy (REASONED; docs): --serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none. -- artifact-existing (REASONED; docs): Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly. -- artifact-direct (REASONED; docs): With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets. -- verify-bind (REASONED; source, iproute2): Read every listener and require 5000 on 127.0.0.1. -- verify-network (REASONED; curl, docs): Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive. -- verify-public-auth (REASONED; docs, curl): POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass. -- verify-backend-auth (REASONED; docs): Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement. -- verify-authorization (REASONED; docs): Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is 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. `mlflow server` serves the tracking UI and REST API at `http://127.0.0.1:5000` (as of v3.16.1, unless `MLFLOW_HOST` or `MLFLOW_PORT` is set) and performs no authentication: anyone who can reach the port can read, alter, and delete experiments, runs, registered models, and (with artifact proxying on) the artifacts themselves. Authentication is opt-in through a separate app, and the server has no dedicated TLS flags (though `mlflow server --uvicorn-opts` can forward `--ssl-keyfile`/`--ssl-certfile` to the default Uvicorn server); MLflow's tracking-server documentation recommends a reverse proxy or VPN for both. @@ -44177,6 +46492,228 @@ REASONED: preceding block; listener inventory, external TCP reachability, proxy/ - oauth2-proxy v7.15.4 provider and upstream configuration; these entries do not establish MFA enforcement (checked October 2026): https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108, https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274 - Authelia v4.39.28 first matching rule, default policy and two_factor requirement (checked October 2026): https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507, https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537 + + +## 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/mlflow.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): + +- source: MLflow pinned source (v3.16.1) +- middleware-min: MLflow security middleware minimum (3.5.0) +- docs: MLflow documentation (unknown) +- docker: Docker Engine loopback publication minimum (28.0.0) +- nginx: nginx proxy implementation (release-1.28.0) +- caddy: Caddy reverse proxy parser (v2.11.4) +- cf-docs: Cloudflare documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104) +- oauth2-proxy-source: oauth2-proxy documentation (v7.15.4) +- authelia-source: Authelia source (v4.39.28) +- curl: curl minimum write-out version (7.75.0) +- iproute2: iproute2 ss manual (v6.12.0) + +Claims (full source mapping in the JSON block below): + +- default-host (REASONED; source): v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting. +- default-port (REASONED; source): v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it. +- default-auth (REASONED; docs): Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts. +- container-bind (REASONED; docker, docs): Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports. +- middleware-scope (REASONED; middleware-min, docs): Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it. +- host-allowlist (REASONED; docs): Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules. +- cors (REASONED; docs): Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin. +- middleware-disable (REASONED; docs): Do not use --disable-security-middleware outside a test. +- tls (REASONED; docs, nginx, caddy): There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname. +- client-tls (REASONED; docs): Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production. +- auth-app (REASONED; docs): Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label. +- csrf-key (REASONED; docs): Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control. +- env-file (REASONED; docs, source): Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching. +- key-exposure (REASONED; source, docs): Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure. +- admin-bootstrap (REASONED; docs): No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply. +- default-permission (REASONED; docs): default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant. +- auth-database (REASONED; docs): database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example. +- auth-config (REASONED; docs): MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic. +- password-rotation (REASONED; docs, curl): PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping. +- user-creation (REASONED; docs): User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user. +- client-credentials (REASONED; docs): ~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD. +- rate-limit (REASONED; docs): The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS. +- mfa (REASONED; docs, cf-docs, oauth2-proxy-source, authelia-source): MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination. +- proxy-token (REASONED; docs, source, cf-docs): MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together. +- artifact-proxy (REASONED; docs): --serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none. +- artifact-existing (REASONED; docs): Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly. +- artifact-direct (REASONED; docs): With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets. +- verify-bind (REASONED; source, iproute2): Read every listener and require 5000 on 127.0.0.1. +- verify-network (REASONED; curl, docs): Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive. +- verify-public-auth (REASONED; docs, curl): POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass. +- verify-backend-auth (REASONED; docs): Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement. +- verify-authorization (REASONED; docs): Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "ee30ff570fece0f829e8b7a87b1a777387f0b1f32d2e160049713119d344088e", + "components": { + "source": { + "name": "MLflow pinned source", + "basis": "v3.16.1", + "sources": { + "sf7285d252e29": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/cli_args.py#L162-L180", + "sa3993a9b625f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L369-L541", + "sd775f10c4d07": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/auth/__init__.py#L6008-L6021", + "s349a81b9478e": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/server/__init__.py#L401-L402", + "s99d30948902f": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L85-L86", + "s687f1c9977cd": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/cli/__init__.py#L94-L105", + "s1661739df39d": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/environment_variables.py#L493-L503", + "s5801c6ac1ac2": "https://github.com/mlflow/mlflow/blob/v3.16.1/mlflow/utils/rest_utils.py#L254-L292" + } + }, + "middleware-min": { + "name": "MLflow security middleware minimum", + "basis": "3.5.0", + "sources": { + "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server" + } + }, + "docs": { + "name": "MLflow documentation", + "basis": "unknown", + "sources": { + "scbbc07258943": "https://mlflow.org/docs/latest/self-hosting/security/basic-http-auth/", + "sc3a2d70cabf9": "https://mlflow.org/docs/latest/api_reference/auth/rest-api.html", + "s5b79d1871e0b": "https://mlflow.org/docs/latest/api_reference/cli.html", + "s2517159af609": "https://mlflow.org/docs/latest/self-hosting/architecture/tracking-server", + "s996e587e947a": "https://mlflow.org/docs/latest/api_reference/rest-api.html", + "s559e7a8271b9": "https://mlflow.org/docs/latest/api_reference/cli.html#mlflow" + } + }, + "docker": { + "name": "Docker Engine loopback publication minimum", + "basis": "28.0.0", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "nginx": { + "name": "nginx proxy implementation", + "basis": "release-1.28.0", + "sources": { + "s6dfa81e36717": "https://github.com/nginx/nginx/blob/release-1.28.0/src/http/modules/ngx_http_proxy_module.c#L305-L310" + } + }, + "caddy": { + "name": "Caddy reverse proxy parser", + "basis": "v2.11.4", + "sources": { + "s52039756d4f4": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L40", + "s9dde4a2bda4c": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/reverseproxy/caddyfile.go#L58-L62" + } + }, + "cf-docs": { + "name": "Cloudflare 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", + "s7e793d13cf31": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/access-settings/independent-mfa.mdx#L15-L24", + "sbec4996a7089": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L33", + "s543b1f2eb29c": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L45-L49", + "sc565f2d80e6e": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/service-credentials/service-tokens.mdx#L59" + } + }, + "oauth2-proxy-source": { + "name": "oauth2-proxy documentation", + "basis": "v7.15.4", + "sources": { + "sa403e3d826b4": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L94-L108", + "se7a200583def": "https://github.com/oauth2-proxy/oauth2-proxy/blob/v7.15.4/docs/docs/configuration/overview.md#L274" + } + }, + "authelia-source": { + "name": "Authelia source", + "basis": "v4.39.28", + "sources": { + "s768b165b44a0": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L506-L507", + "sd4dd76e70475": "https://github.com/authelia/authelia/blob/v4.39.28/docs/content/configuration/security/access-control.md#L536-L537" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "iproute2": { + "name": "iproute2 ss manual", + "basis": "v6.12.0", + "sources": { + "s2afc4764d5bb": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L33-L34", + "sfaa932c2ca13": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L43-L44", + "sd295f82d643f": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L162-L163", + "s65278ee45850": "https://github.com/iproute2/iproute2/blob/v6.12.0/man/man8/ss.8#L374-L375" + } + } + }, + "claims": { + "default-host": {"text": "v3.16.1 defaults to 127.0.0.1 unless MLFLOW_HOST overrides it; explicitly bind privately and do not treat --host as an authentication setting.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, + "default-port": {"text": "v3.16.1 serves the tracking UI and REST API on port 5000 unless MLFLOW_PORT overrides it.", "components": ["source"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f"], "status": "REASONED"}, + "default-auth": {"text": "Authentication is opt-in; a reachable server without the auth app permits reading, changing and deleting tracking resources and proxied artifacts.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s2517159af609"], "status": "REASONED"}, + "container-bind": {"text": "Normal bridge NAT requires host loopback publication 127.0.0.1:5000:5000 and --host 0.0.0.0 inside the container; container loopback cannot serve the publication. Before Docker 28.0.0, same-layer-2 hosts could reach loopback-published ports.", "components": ["docker", "docs"], "sources": ["docker:s1e53417c513d", "docs:s5b79d1871e0b"], "status": "REASONED"}, + "middleware-scope": {"text": "Security middleware requires MLflow 3.5.0 or newer and the default Uvicorn server; --gunicorn-opts and --waitress-opts do not use it.", "components": ["middleware-min", "docs"], "sources": ["middleware-min:s2517159af609", "docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "host-allowlist": {"text": "Set --allowed-hosts mlflow.example.com for access beyond loopback; defaults admit localhost, 127.0.0.1, [::1], 0.0.0.0 and private ranges as Host headers, not client-source firewall rules.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "cors": {"text": "Set --cors-allowed-origins https://mlflow.example.com for the intended browser origin.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "middleware-disable": {"text": "Do not use --disable-security-middleware outside a test.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "tls": {"text": "There are no dedicated server TLS flags; --uvicorn-opts can forward --ssl-keyfile/--ssl-certfile. Use a TLS proxy to 127.0.0.1:5000 or a tunnel/tailnet and set the allowed public hostname.", "components": ["docs", "nginx", "caddy"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609", "nginx:s6dfa81e36717", "caddy:s52039756d4f4", "caddy:s9dde4a2bda4c"], "status": "REASONED"}, + "client-tls": {"text": "Use MLFLOW_TRACKING_URI=https://mlflow.example.com and never enable MLFLOW_TRACKING_INSECURE_TLS in production.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "auth-app": {"text": "Install mlflow[auth] and run --app-name basic-auth; client username/password variables alone enable no server authentication. The September 2026 documentation has no experimental label.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "csrf-key": {"text": "Provision a long random MLFLOW_FLASK_SERVER_SECRET_KEY identical on every replica through a protected server.env file, mode 0600, with protected directories and no other-account ACL access; keep file and backups out of source control.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "env-file": {"text": "Global --env-file loads dotenv before the server command without overriding existing environment; clear inherited key/config-path variables and require a readable regular non-symlink file before launching.", "components": ["docs", "source"], "sources": ["docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, + "key-exposure": {"text": "Only the file path reaches the launch command; MLflow v3.16.1 loads the key into its environment and forwards it to workers, retaining same-account/root memory and inheriting-process environment exposure.", "components": ["source", "docs"], "sources": ["source:sd775f10c4d07", "source:s349a81b9478e", "docs:s559e7a8271b9", "source:s99d30948902f", "source:s687f1c9977cd"], "status": "REASONED"}, + "admin-bootstrap": {"text": "No default admin password: first start requires at least 12 characters from MLFLOW_AUTH_ADMIN_PASSWORD or admin_password, rejects password1234 and fails without a password; an already-bootstrapped admin needs no resupply.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "default-permission": {"text": "default_permission is READ on every resource; set NO_PERMISSIONS in the auth configuration to remove that default grant.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "auth-database": {"text": "database_uri defaults to basic_auth.db in the working directory; use a central database for multiple nodes, as in the PostgreSQL auth-database example.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "auth-config": {"text": "MLFLOW_AUTH_CONFIG_PATH selects basic_auth.ini; authorization_function accepts module:function for custom authentication, while the shipped scheme is HTTP Basic.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "password-rotation": {"text": "PATCH /api/2.0/mlflow/users/update-password takes username and password; the example feeds current and new passwords through curl config stdin with JSON/config escaping.", "components": ["docs", "curl"], "sources": ["docs:sc3a2d70cabf9", "curl:s2b2686afaf41"], "status": "REASONED"}, + "user-creation": {"text": "User creation requires admin credentials at /signup or POST /api/2.0/mlflow/users/create; give humans individual accounts and CI a separate low-permission user.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:sc3a2d70cabf9"], "status": "REASONED"}, + "client-credentials": {"text": "~/.mlflow/credentials stores passwords unencrypted; prefer runtime-injected MLFLOW_TRACKING_USERNAME/MLFLOW_TRACKING_PASSWORD.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "rate-limit": {"text": "The UI has no login-attempt limit and Basic auth is checked on every protected API request; rate-limit all authentication-bearing routes at the proxy and protect password transport with TLS.", "components": ["docs"], "sources": ["docs:scbbc07258943"], "status": "REASONED"}, + "mfa": {"text": "MFA: this guide's cited MLflow material does not establish MFA support in the basic-auth app. Guide advice: put an identity-aware layer in front (Cloudflare Access, Authelia, oauth2-proxy per mfa.md) and require MFA for human access. Cloudflare Access at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 supports IdP-based MFA requirements for Okta, Microsoft Entra ID, Generic OIDC and Generic SAML 2.0, rejecting a user who does not present the required method, or independent MFA with methods including TOTP and WebAuthn security keys; independent MFA must first be enabled at organization level, and its effective requirement follows policy over application over organization. Authelia v4.39.28 applies the first matching access-control rule, or default_policy if none matches; its two_factor policy requires successful 2FA. oauth2-proxy v7.15.4 documents an OAuth provider and upstream endpoints, not second-factor enforcement in these passages; guide advice: require MFA at the identity provider for this login and test enforcement there. These sources do not establish MFA enforcement in a deployed MLflow combination.", "components": ["docs", "cf-docs", "oauth2-proxy-source", "authelia-source"], "sources": ["docs:scbbc07258943", "cf-docs:s6f6104362214", "cf-docs:s7e793d13cf31", "oauth2-proxy-source:sa403e3d826b4", "oauth2-proxy-source:se7a200583def", "authelia-source:s768b165b44a0", "authelia-source:sd4dd76e70475"], "status": "REASONED"}, + "proxy-token": {"text": "MLflow v3.16.1 declares MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD and MLFLOW_TRACKING_TOKEN as tracking authentication inputs; in the cited HTTP request-construction path, nonempty username and password credentials select an Authorization: Basic header unless the auth mode is kubernetes or kubernetes-namespaced, otherwise a nonempty token selects Authorization: Bearer. The token variable's comment claims precedence, but that request code selects Basic first under those conditions; AWS SigV4 overwrites Authorization, and a configured auth handler is applied separately, so this is not a guarantee for every authentication mode. Cloudflare Access service tokens at c896795b8e8bab68d05ae78d64c9cbcda1fdb104 use CF-Access-Client-Id and CF-Access-Client-Secret headers, with a Service Auth policy required to avoid an IdP login prompt; a self-hosted Access application can alternatively be configured to accept the service token in one header. Guide advice: confirm that the chosen client and proxy combination preserves both proxy authentication and MLflow authentication; these citations do not establish that MLFLOW_TRACKING_TOKEN alone supplies Cloudflare service-token headers or that both layers work together.", "components": ["docs", "source", "cf-docs"], "sources": ["docs:s2517159af609", "source:s1661739df39d", "source:s5801c6ac1ac2", "cf-docs:sbec4996a7089", "cf-docs:s543b1f2eb29c", "cf-docs:sc565f2d80e6e"], "status": "REASONED"}, + "artifact-proxy": {"text": "--serve-artifacts defaults on; --artifacts-destination s3://bucket lets the server proxy reads/writes for proxied experiments, holding storage credentials so clients need none.", "components": ["docs"], "sources": ["docs:s5b79d1871e0b", "docs:s2517159af609"], "status": "REASONED"}, + "artifact-existing": {"text": "Pre-existing experiments and explicit direct artifact locations retain their locations outside tracking-server permission enforcement; inspect locations and apply storage IAM directly.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "artifact-direct": {"text": "With --no-serve-artifacts every client needs storage credentials and tracking-server permissions no longer gate artifacts; use environment or instance-role credentials, not repository/Compose secrets.", "components": ["docs"], "sources": ["docs:s2517159af609"], "status": "REASONED"}, + "verify-bind": {"text": "Read every listener and require 5000 on 127.0.0.1.", "components": ["source", "iproute2"], "sources": ["source:sf7285d252e29", "source:sa3993a9b625f", "iproute2:s2afc4764d5bb", "iproute2:sfaa932c2ca13", "iproute2:sd295f82d643f", "iproute2:s65278ee45850"], "status": "REASONED", "verify": [1]}, + "verify-network": {"text": "Outside :5000 must form no TCP connection: time_connect stays 0.000000 with a connection-level failure. A handshake or HTTP response means exposure; DNS/local socket errors are inconclusive.", "components": ["curl", "docs"], "sources": ["curl:s2b2686afaf41", "docs:s5b79d1871e0b"], "status": "REASONED", "verify": [1]}, + "verify-public-auth": {"text": "POST experiments/search with a minimal body: anonymous and wrong-password requests should return 401, valid admin 200; an open backend returns anonymous 200. Proxy credentials may also be needed; shell success is not a pass.", "components": ["docs", "curl"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [1]}, + "verify-backend-auth": {"text": "Repeat anonymous 401/admin 200 directly on loopback with Host: mlflow.example.com; an allowlist Host rejection is 403 before auth, and a proxy-only denial proves no backend enforcement.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s5b79d1871e0b", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]}, + "verify-authorization": {"text": "Search filters visible experiments and does not prove authorization denial. First GET a real experiment as admin for 200, then the same backend resource as a low-permission user for 403; 401 means bad credentials and a failed admin control is inconclusive.", "components": ["docs"], "sources": ["docs:scbbc07258943", "docs:s996e587e947a"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> agent-builders.md ====================================================================== @@ -48889,32 +51426,168 @@ Claims (full source mapping in the JSON block below): # Admin panels: phpMyAdmin, pgAdmin, mongo-express, Grafana, Prometheus +Written against phpMyAdmin (no recorded version), Grafana v13.2.2, Prometheus v3.14.0, RedisInsight 3.8.0, Apache htpasswd 2.4, Apache httpd htpasswd source 2.4.69, and Moby port mapping source v28.3.3; 2 further sources are pinned to a commit and 6 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 20 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. + +Database and monitoring panels are the most-scanned targets on the internet, and several ship with known default credentials. One rule dominates everything tool-specific below: **an admin panel is never reachable from the public internet.** Bind it to loopback and reach it through SSH port forwarding, a VPN or tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) plus MFA ([mfa.md](mfa.md)). + +## mongo-express + +The web login is off by default on recent releases, and the enabling variable has changed across mongo-express versions: current releases read `ME_CONFIG_BASICAUTH_ENABLED` (the README marks the older `ME_CONFIG_BASICAUTH` deprecated but still honors it as a fallback), while some 1.0.x builds instead key the login off a non-empty `ME_CONFIG_BASICAUTH_USERNAME`. Rather than track the exact version boundary, set both flags, set your own non-empty credentials, and VERIFY that an unauthenticated request is rejected, because mongo-express can otherwise fall back to the widely-scanned `admin`:`pass`. Keep it private: + +``` +ME_CONFIG_BASICAUTH=true # older, now-deprecated enable flag, still honored as a fallback +ME_CONFIG_BASICAUTH_ENABLED=true # the current enable flag; set both, then verify the login actually appears +ME_CONFIG_BASICAUTH_USERNAME= # otherwise defaults to admin +ME_CONFIG_BASICAUTH_PASSWORD= # otherwise defaults to pass +``` + +These control only the web login; MongoDB credentials go in `ME_CONFIG_MONGODB_URL` ([mongodb.md](mongodb.md) hardens the database itself). + +## Grafana + +- First sign-in uses `admin`/`admin` and prompts for a new password; set a strong one immediately and create individual accounts for everyone else. +- Disable anonymous access if it was enabled, and prefer SSO with MFA enforced at the identity provider. +- Native HTTPS in `grafana.ini`: + +```ini +[server] +protocol = https +# bind loopback (Grafana's default http_addr is empty as of v13.2.2, which means all interfaces); reach it via a tunnel or proxy +http_addr = 127.0.0.1 +cert_file = /etc/grafana/grafana.crt +cert_key = /etc/grafana/grafana.key +``` + +## Prometheus + +No authentication at all by default, and it listens on `0.0.0.0:9090` by default (as of v3.14.0). Give it a web configuration file and bind it to loopback, starting with `--web.listen-address=127.0.0.1:9090 --web.config.file=web.yml`: + +```yaml +basic_auth_users: + admin: $2b$12$REPLACE_WITH_BCRYPT_HASH # htpasswd -nB -C 12 admin, hash part (Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5; by this guide's comparison, below OWASP's minimum 10 for legacy bcrypt) +``` + +The same file carries TLS (`tls_server_config` with `cert_file` and `key_file`; see the Prometheus TLS guide below). Validate with `promtool check web-config web.yml`. Exporters and Alertmanager need the same treatment; see [observability-components.md](observability-components.md). + +## phpMyAdmin and pgAdmin + +Neither belongs on a public vhost. Serve them only behind the proxy-level TLS and authentication of your web server guide, restrict by source IP where the proxy supports it, and keep them updated; both are perennial exploit targets. pgAdmin in server mode has its own login; treat its accounts per [authentication.md](authentication.md). + +## Adminer + +Adminer is database management in a single PHP file, and a filename like `adminer.php` or `adminer-.php` is not an access control; renaming it or hiding the directory does not satisfy the never-public rule, since scanners enumerate those paths. It has no built-in account store of its own: by default the login form takes the database server's own address, username, and password, so an internet-reachable Adminer is a public interface for logging in to your database from the Adminer host. Because Adminer connects to the database server-side, that exposes the database to internet login attempts even when the database itself is not reachable from the internet, and older versions carry their own exploit history. The rule at the top of this guide applies without exception: keep it off a public vhost, reach it privately or behind the proxy's TLS, authentication, and MFA, restrict by source IP where you can, keep it updated, and delete the file (and any other copy) from the docroot when you are not using it. + +## RedisInsight and similar tools + +As this guide's advice, keep them on loopback or a private network and reach them through the tunnels above. When in doubt, apply the generic pattern: loopback bind, TLS proxy, proxy or SSO authentication, MFA. + +RedisInsight 3.8.0's default server configuration uses `RI_APP_HOST` with a fallback of `0.0.0.0` and `RI_APP_PORT` with a fallback of `5540`; its default `excludeAuthRoutes` is an empty list. The fetched `SingleUserAuthMiddleware.use` creates a default session if absent, assigns default session metadata and calls `next()`; `WindowAuthMiddleware.use` checks a window ID through `WindowAuthService` and throws on rejection. These fetched files do not establish the complete inbound authentication policy or which routes use these middleware; they do not establish either the absence of authentication or login and dashboard pages both returning 200. RedisInsight is therefore omitted from the Verify list asserting those two 200 responses. + +## Verify + +REASONED: following block; panel sources below support the listener and anonymous front-page/data-route checks; Docker's pinned documentation supports explicit publish addresses and Linux firewall rules for publication. Docker documents IPv6-to-IPv4 mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default). The fetched Moby port-mapping source starts the userland proxy conditionally and listens on bound TCP sockets even without a proxy; these fetched sources do not establish NAT publication without a host listener. As this guide's assessment, a socket listing alone does not prove reachability. No panel deployment or external test network is available here; no live run is recorded. The comments give expected denial, exposure and inconclusive outcomes. + +```bash +# On the host: each panel listener should be loopback (127.0.0.1) or a deliberately chosen private address. +# Docker documents explicit publish addresses and firewall rules for publication. As this guide's assessment, +# a socket listing alone does not prove reachability, so also inspect the actual published addresses and +# ports (docker ps and the port mappings) and probe any public backend mapping too: +# a protected frontend does not prove a separately published backend port is closed. +ss -tlnp # panel listeners: loopback or a private address only + +# From OUTSIDE your network, with client proxies disabled so a proxy cannot answer for the panel. Read the +# BODY, not just the status: a login page and a dashboard can both be 200. +curl -q -g -sS --noproxy '*' -i 'https://panel.example.com/' +# PASS: a denial from whatever protects this panel - the panel's own 401/403, the reverse proxy's, or a +# Cloudflare Access challenge - or a redirect into that layer's login (which may be an external identity +# provider). FINDING: a 200 that renders the dashboard or app content. An arbitrary redirect is inconclusive. +# SPA panels (Grafana, a phpMyAdmin/Adminer login shell) return 200 for BOTH the login page and +# the dashboard, so also request a route you know returns real data when authorized, and require it be denied +# without credentials: +curl -q -g -sS --noproxy '*' -i 'https://panel.example.com/REPLACE_WITH_PROTECTED_PATH' # keep the quotes; substitute inside them +# Confirm that route returns data WITH valid auth first (a positive control); a 404, a server error, or an +# unsubstituted placeholder is inconclusive, not a pass. +``` + +REASONED: preceding block; listener inventory, anonymous front-page/data-route checks and an authenticated data-route positive control follow the cited panel sources; published-backend inspection follows the pinned Docker sources and this guide's assessment that a protected frontend or absent socket does not prove backend isolation. No panel deployment or second test network is available. Run these from a second network against each panel's real hostname; a 200 that renders a dashboard, or any content past the login, without credentials is a finding, while a proxy, DNS, or TLS error is inconclusive, not a pass. + +## Sources (checked September 2026) + +- mongo-express README (defaults and variables): https://github.com/mongo-express/mongo-express +- Grafana configuration and HTTPS: https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ and https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/ +- Prometheus basic auth and TLS guides: https://prometheus.io/docs/guides/basic-auth/ and https://prometheus.io/docs/guides/tls-encryption/ +- phpMyAdmin documentation: https://www.phpmyadmin.net/docs/ and pgAdmin documentation: https://www.pgadmin.org/docs/ +- Adminer, database management in a single PHP file (login uses the database server's own credentials): https://www.adminer.org/ +- Prometheus `--web.listen-address` default `0.0.0.0:9090` (pinned tag v3.14.0): https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425 +- Grafana default `http_addr` is empty (pinned tag v13.2.2): https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50 +- Grafana joins `http_addr` with the port into the server address (pinned tag v13.2.2): https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469 +- Grafana listens on that address with `net.Listen("tcp", ...)` (pinned tag v13.2.2): https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568 +- Go `net.Listen` with an empty host listens on all available unicast and anycast addresses of the local system: https://pkg.go.dev/net#Listen +- Apache 2.4 `htpasswd` bcrypt default cost and `-C` override (checked October 2026): https://httpd.apache.org/docs/2.4/programs/htpasswd.html +- Apache httpd 2.4.69 htpasswd bcrypt default cost (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45 +- Apache httpd 2.4.69 htpasswd default cost applied (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295 +- Apache httpd 2.4.69 htpasswd `-C` cost option (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383 +- Apache httpd 2.4.69 htpasswd usage (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129 +- 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 +- Apache httpd 2.4.69 htpasswd zero-initialized cost (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297 +- RedisInsight 3.8.0 default listen host and port (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100 +- RedisInsight 3.8.0 default authentication-route exclusions (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133 +- RedisInsight 3.8.0 default-session middleware (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49 +- RedisInsight 3.8.0 window-authentication middleware (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35 +- Docker explicit publish addresses, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38 +- Docker conditional 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 Linux firewall rules for publication, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 +- Moby port mapping at v28.3.3, conditional proxy startup and TCP listening without a proxy (checked October 2026): https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.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/admin-uis.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): +- phpmyadmin: phpMyAdmin (unknown) +- grafana-pin: Grafana (v13.2.2) +- prometheus-pin: Prometheus (v3.14.0) +- pgadmin: pgAdmin (unknown) - mongo: mongo-express (unknown) - grafana: Grafana docs (unknown) - prometheus: Prometheus docs (unknown) -- phpmyadmin: phpMyAdmin (unknown) -- pgadmin: pgAdmin (unknown) - adminer: Adminer (unknown) -- prometheus-pin: Prometheus (v3.14.0) -- grafana-pin: Grafana (v13.2.2) +- redisinsight: RedisInsight (3.8.0) - go: Go net (unknown) - htpasswd: Apache htpasswd (2.4) - httpd-source: Apache httpd htpasswd source (2.4.69) -- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) -- redisinsight: RedisInsight (3.8.0) - engine-pinned: Docker Engine networking documentation (4e9a5751518ed8223a8dcde53693badddd72604f) - moby-pinned: Moby port mapping source (v28.3.3) +- owasp-source: OWASP Password Storage Cheat Sheet (72865b1e3b5754246a101778dc00b37f9c233373) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): - private (REASONED; grafana, prometheus, phpmyadmin, pgadmin, adminer, redisinsight): Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice. - mongo-enable (REASONED; mongo): Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify. @@ -48936,118 +51609,158 @@ Claims (full source mapping in the front matter): - verify-listeners (REASONED; grafana-pin, prometheus-pin, engine-pinned, moby-pinned): Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy. - verify-login (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive. - verify-data (REASONED; mongo, grafana, prometheus, phpmyadmin, pgadmin, adminer): A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive. - - -Database and monitoring panels are the most-scanned targets on the internet, and several ship with known default credentials. One rule dominates everything tool-specific below: **an admin panel is never reachable from the public internet.** Bind it to loopback and reach it through SSH port forwarding, a VPN or tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) plus MFA ([mfa.md](mfa.md)). - -## mongo-express - -The web login is off by default on recent releases, and the enabling variable has changed across mongo-express versions: current releases read `ME_CONFIG_BASICAUTH_ENABLED` (the README marks the older `ME_CONFIG_BASICAUTH` deprecated but still honors it as a fallback), while some 1.0.x builds instead key the login off a non-empty `ME_CONFIG_BASICAUTH_USERNAME`. Rather than track the exact version boundary, set both flags, set your own non-empty credentials, and VERIFY that an unauthenticated request is rejected, because mongo-express can otherwise fall back to the widely-scanned `admin`:`pass`. Keep it private: - -``` -ME_CONFIG_BASICAUTH=true # older, now-deprecated enable flag, still honored as a fallback -ME_CONFIG_BASICAUTH_ENABLED=true # the current enable flag; set both, then verify the login actually appears -ME_CONFIG_BASICAUTH_USERNAME= # otherwise defaults to admin -ME_CONFIG_BASICAUTH_PASSWORD= # otherwise defaults to pass -``` - -These control only the web login; MongoDB credentials go in `ME_CONFIG_MONGODB_URL` ([mongodb.md](mongodb.md) hardens the database itself). - -## Grafana - -- First sign-in uses `admin`/`admin` and prompts for a new password; set a strong one immediately and create individual accounts for everyone else. -- Disable anonymous access if it was enabled, and prefer SSO with MFA enforced at the identity provider. -- Native HTTPS in `grafana.ini`: - -```ini -[server] -protocol = https -# bind loopback (Grafana's default http_addr is empty as of v13.2.2, which means all interfaces); reach it via a tunnel or proxy -http_addr = 127.0.0.1 -cert_file = /etc/grafana/grafana.crt -cert_key = /etc/grafana/grafana.key -``` - -## Prometheus - -No authentication at all by default, and it listens on `0.0.0.0:9090` by default (as of v3.14.0). Give it a web configuration file and bind it to loopback, starting with `--web.listen-address=127.0.0.1:9090 --web.config.file=web.yml`: - -```yaml -basic_auth_users: - admin: $2b$12$REPLACE_WITH_BCRYPT_HASH # htpasswd -nB -C 12 admin, hash part (Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5; by this guide's comparison, below OWASP's minimum 10 for legacy bcrypt) -``` - -The same file carries TLS (`tls_server_config` with `cert_file` and `key_file`; see the Prometheus TLS guide below). Validate with `promtool check web-config web.yml`. Exporters and Alertmanager need the same treatment; see [observability-components.md](observability-components.md). - -## phpMyAdmin and pgAdmin - -Neither belongs on a public vhost. Serve them only behind the proxy-level TLS and authentication of your web server guide, restrict by source IP where the proxy supports it, and keep them updated; both are perennial exploit targets. pgAdmin in server mode has its own login; treat its accounts per [authentication.md](authentication.md). - -## Adminer - -Adminer is database management in a single PHP file, and a filename like `adminer.php` or `adminer-.php` is not an access control; renaming it or hiding the directory does not satisfy the never-public rule, since scanners enumerate those paths. It has no built-in account store of its own: by default the login form takes the database server's own address, username, and password, so an internet-reachable Adminer is a public interface for logging in to your database from the Adminer host. Because Adminer connects to the database server-side, that exposes the database to internet login attempts even when the database itself is not reachable from the internet, and older versions carry their own exploit history. The rule at the top of this guide applies without exception: keep it off a public vhost, reach it privately or behind the proxy's TLS, authentication, and MFA, restrict by source IP where you can, keep it updated, and delete the file (and any other copy) from the docroot when you are not using it. - -## RedisInsight and similar tools - -As this guide's advice, keep them on loopback or a private network and reach them through the tunnels above. When in doubt, apply the generic pattern: loopback bind, TLS proxy, proxy or SSO authentication, MFA. - -RedisInsight 3.8.0's default server configuration uses `RI_APP_HOST` with a fallback of `0.0.0.0` and `RI_APP_PORT` with a fallback of `5540`; its default `excludeAuthRoutes` is an empty list. The fetched `SingleUserAuthMiddleware.use` creates a default session if absent, assigns default session metadata and calls `next()`; `WindowAuthMiddleware.use` checks a window ID through `WindowAuthService` and throws on rejection. These fetched files do not establish the complete inbound authentication policy or which routes use these middleware; they do not establish either the absence of authentication or login and dashboard pages both returning 200. RedisInsight is therefore omitted from the Verify list asserting those two 200 responses. -## Verify - -REASONED: following block; panel sources below support the listener and anonymous front-page/data-route checks; Docker's pinned documentation supports explicit publish addresses and Linux firewall rules for publication. Docker documents IPv6-to-IPv4 mapping when no host IP is given, the bridge is IPv4-only and `--userland-proxy=true` (the default). The fetched Moby port-mapping source starts the userland proxy conditionally and listens on bound TCP sockets even without a proxy; these fetched sources do not establish NAT publication without a host listener. As this guide's assessment, a socket listing alone does not prove reachability. No panel deployment or external test network is available here; no live run is recorded. The comments give expected denial, exposure and inconclusive outcomes. - -```bash -# On the host: each panel listener should be loopback (127.0.0.1) or a deliberately chosen private address. -# Docker documents explicit publish addresses and firewall rules for publication. As this guide's assessment, -# a socket listing alone does not prove reachability, so also inspect the actual published addresses and -# ports (docker ps and the port mappings) and probe any public backend mapping too: -# a protected frontend does not prove a separately published backend port is closed. -ss -tlnp # panel listeners: loopback or a private address only - -# From OUTSIDE your network, with client proxies disabled so a proxy cannot answer for the panel. Read the -# BODY, not just the status: a login page and a dashboard can both be 200. -curl -q -g -sS --noproxy '*' -i 'https://panel.example.com/' -# PASS: a denial from whatever protects this panel - the panel's own 401/403, the reverse proxy's, or a -# Cloudflare Access challenge - or a redirect into that layer's login (which may be an external identity -# provider). FINDING: a 200 that renders the dashboard or app content. An arbitrary redirect is inconclusive. -# SPA panels (Grafana, a phpMyAdmin/Adminer login shell) return 200 for BOTH the login page and -# the dashboard, so also request a route you know returns real data when authorized, and require it be denied -# without credentials: -curl -q -g -sS --noproxy '*' -i 'https://panel.example.com/REPLACE_WITH_PROTECTED_PATH' # keep the quotes; substitute inside them -# Confirm that route returns data WITH valid auth first (a positive control); a 404, a server error, or an -# unsubstituted placeholder is inconclusive, not a pass. -``` - -REASONED: preceding block; listener inventory, anonymous front-page/data-route checks and an authenticated data-route positive control follow the cited panel sources; published-backend inspection follows the pinned Docker sources and this guide's assessment that a protected frontend or absent socket does not prove backend isolation. No panel deployment or second test network is available. Run these from a second network against each panel's real hostname; a 200 that renders a dashboard, or any content past the login, without credentials is a finding, while a proxy, DNS, or TLS error is inconclusive, not a pass. - -## Sources (checked September 2026) - -- mongo-express README (defaults and variables): https://github.com/mongo-express/mongo-express -- Grafana configuration and HTTPS: https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/ and https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/ -- Prometheus basic auth and TLS guides: https://prometheus.io/docs/guides/basic-auth/ and https://prometheus.io/docs/guides/tls-encryption/ -- phpMyAdmin documentation: https://www.phpmyadmin.net/docs/ and pgAdmin documentation: https://www.pgadmin.org/docs/ -- Adminer, database management in a single PHP file (login uses the database server's own credentials): https://www.adminer.org/ -- Prometheus `--web.listen-address` default `0.0.0.0:9090` (pinned tag v3.14.0): https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425 -- Grafana default `http_addr` is empty (pinned tag v13.2.2): https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50 -- Grafana joins `http_addr` with the port into the server address (pinned tag v13.2.2): https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469 -- Grafana listens on that address with `net.Listen("tcp", ...)` (pinned tag v13.2.2): https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568 -- Go `net.Listen` with an empty host listens on all available unicast and anycast addresses of the local system: https://pkg.go.dev/net#Listen -- Apache 2.4 `htpasswd` bcrypt default cost and `-C` override (checked October 2026): https://httpd.apache.org/docs/2.4/programs/htpasswd.html -- Apache httpd 2.4.69 htpasswd bcrypt default cost (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.h#L45 -- Apache httpd 2.4.69 htpasswd default cost applied (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L292-L295 -- Apache httpd 2.4.69 htpasswd `-C` cost option (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383 -- Apache httpd 2.4.69 htpasswd usage (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129 -- 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 -- Apache httpd 2.4.69 htpasswd zero-initialized cost (checked October 2026): https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297 -- RedisInsight 3.8.0 default listen host and port (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100 -- RedisInsight 3.8.0 default authentication-route exclusions (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133 -- RedisInsight 3.8.0 default-session middleware (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49 -- RedisInsight 3.8.0 window-authentication middleware (checked October 2026): https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35 -- Docker explicit publish addresses, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38 -- Docker conditional 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 Linux firewall rules for publication, at 4e9a5751518ed8223a8dcde53693badddd72604f (checked October 2026): https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15 -- Moby port mapping at v28.3.3, conditional proxy startup and TCP listening without a proxy (checked October 2026): https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "92d3d0f412f545709088bb0ce3d9b82c95988187a6fd4fcf75747d818f8d5c8b", + "components": { + "phpmyadmin": { + "name": "phpMyAdmin", + "basis": "unknown", + "sources": { + "s5f6c8965c539": "https://www.phpmyadmin.net/docs/" + } + }, + "grafana-pin": { + "name": "Grafana", + "basis": "v13.2.2", + "sources": { + "s4c6ff3174e7e": "https://github.com/grafana/grafana/blob/v13.2.2/conf/defaults.ini#L49-L50", + "sb00e234d0ddd": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L467-L469", + "sf14b29976adb": "https://github.com/grafana/grafana/blob/v13.2.2/pkg/api/http_server.go#L568" + } + }, + "prometheus-pin": { + "name": "Prometheus", + "basis": "v3.14.0", + "sources": { + "s6d5f308b02a2": "https://github.com/prometheus/prometheus/blob/v3.14.0/cmd/prometheus/main.go#L424-L425" + } + }, + "pgadmin": { + "name": "pgAdmin", + "basis": "unknown", + "sources": { + "s2f7e33714435": "https://www.pgadmin.org/docs/" + } + }, + "mongo": { + "name": "mongo-express", + "basis": "unknown", + "sources": { + "s69ec4eee2c85": "https://github.com/mongo-express/mongo-express" + } + }, + "grafana": { + "name": "Grafana docs", + "basis": "unknown", + "sources": { + "s7aa447405cc5": "https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/", + "sc241e0772095": "https://grafana.com/docs/grafana/latest/setup-grafana/set-up-https/" + } + }, + "prometheus": { + "name": "Prometheus docs", + "basis": "unknown", + "sources": { + "scce074c47e9c": "https://prometheus.io/docs/guides/basic-auth/", + "sc48b506cc29a": "https://prometheus.io/docs/guides/tls-encryption/" + } + }, + "adminer": { + "name": "Adminer", + "basis": "unknown", + "sources": { + "sb5eda9397b18": "https://www.adminer.org/" + } + }, + "redisinsight": { + "name": "RedisInsight", + "basis": "3.8.0", + "sources": { + "s6b8fb1c9bbbc": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L99-L100", + "s7ba57be16bb3": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/config/default.ts#L133", + "sddf9b5ee80b9": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts#L20-L49", + "s5e6e219b4b65": "https://github.com/redis/RedisInsight/blob/3.8.0/redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts#L14-L35" + } + }, + "go": { + "name": "Go net", + "basis": "unknown", + "sources": { + "s37204ff1b27c": "https://pkg.go.dev/net#Listen" + } + }, + "htpasswd": { + "name": "Apache htpasswd", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "httpd-source": { + "name": "Apache httpd htpasswd source", + "basis": "2.4.69", + "sources": { + "sfed14ba2991a": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L297", + "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", + "s4ec6b9b216dc": "https://github.com/apache/httpd/blob/2.4.69/support/passwd_common.c#L373-L383", + "s6952d96564d9": "https://github.com/apache/httpd/blob/2.4.69/support/htpasswd.c#L100-L129" + } + }, + "engine-pinned": { + "name": "Docker Engine networking documentation", + "basis": "4e9a5751518ed8223a8dcde53693badddd72604f", + "sources": { + "sb3dacfbaf7e1": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L33-L38", + "s1c82ff316a7e": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L60-L62", + "sc244607b20e4": "https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/packet-filtering-firewalls.md#L11-L15" + } + }, + "moby-pinned": { + "name": "Moby port mapping source", + "basis": "v28.3.3", + "sources": { + "seaf4197aafaa": "https://raw.githubusercontent.com/moby/moby/v28.3.3/libnetwork/drivers/bridge/port_mapping_linux.go" + } + }, + "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": { + "private": {"text": "Keep panels private; public front doors need proxy TLS, authentication and MFA. RedisInsight 3.8.0 config defaults to 0.0.0.0:5540 via RI_APP_HOST/RI_APP_PORT, with empty excludeAuthRoutes; fetched session/window middleware does not establish complete inbound authentication or login/dashboard 200 responses. Keeping RedisInsight and similar tools on loopback or a private network is this guide's advice.", "components": ["grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer", "redisinsight"], "sources": ["grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18", "redisinsight:s6b8fb1c9bbbc", "redisinsight:s7ba57be16bb3", "redisinsight:sddf9b5ee80b9", "redisinsight:s5e6e219b4b65"], "status": "REASONED"}, + "mongo-enable": {"text": "Recent login defaults off; current ENABLED and deprecated BASICAUTH fallback differ from some 1.0.x username-triggered builds. Set both flags and verify.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "mongo-credentials": {"text": "Set nonempty BASICAUTH_USERNAME/PASSWORD instead of admin/pass defaults.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "mongo-database": {"text": "Web Basic auth is separate from MongoDB credentials in ME_CONFIG_MONGODB_URL.", "components": ["mongo"], "sources": ["mongo:s69ec4eee2c85"], "status": "REASONED"}, + "grafana-admin": {"text": "First login uses admin/admin and prompts for replacement; use strong credentials and individual accounts.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-anonymous": {"text": "Disable Grafana anonymous access if enabled.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-mfa": {"text": "Prefer SSO with MFA at the identity provider.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5"], "status": "REASONED"}, + "grafana-bind": {"text": "Empty http_addr joins the port and binds all interfaces; set 127.0.0.1.", "components": ["grafana-pin", "go"], "sources": ["grafana-pin:s4c6ff3174e7e", "grafana-pin:sb00e234d0ddd", "grafana-pin:sf14b29976adb", "go:s37204ff1b27c"], "status": "REASONED"}, + "grafana-tls": {"text": "Native HTTPS uses server protocol=https, cert_file and cert_key.", "components": ["grafana"], "sources": ["grafana:s7aa447405cc5", "grafana:sc241e0772095"], "status": "REASONED"}, + "prometheus-auth": {"text": "Authentication defaults off; web.config.file supplies basic_auth_users with bcrypt hashes.", "components": ["prometheus"], "sources": ["prometheus:scce074c47e9c"], "status": "REASONED"}, + "prometheus-bcrypt": {"text": "Use htpasswd -nB -C 12; Apache httpd 2.4.69 bare -B defaults to bcrypt cost 5, by this guide's comparison below OWASP's minimum work factor of 10 for legacy bcrypt.", "components": ["prometheus", "htpasswd", "httpd-source", "owasp-source"], "sources": ["prometheus:scce074c47e9c", "htpasswd:scde9004bb967", "httpd-source:sfed14ba2991a", "httpd-source:sf711558bdf3c", "httpd-source:s4aa0f7f341fb", "httpd-source:s4ec6b9b216dc", "httpd-source:s6952d96564d9", "owasp-source:s4531e65b6502"], "status": "REASONED"}, + "prometheus-bind": {"text": "Default 0.0.0.0:9090; set --web.listen-address=127.0.0.1:9090.", "components": ["prometheus-pin"], "sources": ["prometheus-pin:s6d5f308b02a2"], "status": "REASONED"}, + "prometheus-tls": {"text": "web.yml tls_server_config cert_file/key_file enable TLS; validate with promtool check web-config.", "components": ["prometheus"], "sources": ["prometheus:sc48b506cc29a"], "status": "REASONED"}, + "database-panels": {"text": "Keep phpMyAdmin/pgAdmin behind proxy TLS/auth, restrict source IPs where supported and keep updated.", "components": ["phpmyadmin", "pgadmin"], "sources": ["phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435"], "status": "REASONED"}, + "pgadmin-login": {"text": "pgAdmin server mode has its own accounts/login.", "components": ["pgadmin"], "sources": ["pgadmin:s2f7e33714435"], "status": "REASONED"}, + "adminer-login": {"text": "Adminer uses database credentials, not a separate account store; server-side connections expose even private databases to login attempts.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, + "adminer-file": {"text": "Renaming Adminer is no access control; require private access or proxy TLS/auth/MFA, updates and removal of unused copies.", "components": ["adminer"], "sources": ["adminer:sb5eda9397b18"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory sockets and Docker publications; as this guide's assessment, a protected frontend or absent socket does not prove backend isolation or reachability. Docker documents explicit publish addresses and IPv6-to-IPv4 proxying when no host IP is given, the bridge is IPv4-only and --userland-proxy=true (default). Fetched sources do not establish NAT publication without a host listener; the fetched Moby TCP path listens even without a proxy.", "components": ["grafana-pin", "prometheus-pin", "engine-pinned", "moby-pinned"], "sources": ["grafana-pin:s4c6ff3174e7e", "prometheus-pin:s6d5f308b02a2", "engine-pinned:sb3dacfbaf7e1", "engine-pinned:s1c82ff316a7e", "engine-pinned:sc244607b20e4", "moby-pinned:seaf4197aafaa"], "status": "REASONED", "verify": [1]}, + "verify-login": {"text": "Anonymous dashboard data is a finding; require denial or verified login challenge. Arbitrary redirects and DNS/TLS/proxy errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]}, + "verify-data": {"text": "A 200 login shell proves nothing; require a known data route to succeed authorized and deny anonymous access; 404/server errors are inconclusive.", "components": ["mongo", "grafana", "prometheus", "phpmyadmin", "pgadmin", "adminer"], "sources": ["mongo:s69ec4eee2c85", "grafana:s7aa447405cc5", "prometheus:scce074c47e9c", "phpmyadmin:s5f6c8965c539", "pgadmin:s2f7e33714435", "adminer:sb5eda9397b18"], "status": "REASONED", "verify": [1]} + } +} +```` + ====================================================================== ==> observability-components.md @@ -49928,102 +52641,9 @@ Claims (full source mapping in the JSON block below): # DevOps panels: Portainer, Coolify, Dokploy, Nginx Proxy Manager, Vaultwarden, Kubernetes Dashboard, Jenkins, Gitea, Uptime Kuma, and the Docker API - -**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 Portainer (no recorded version), Dashboard arguments v2.7.0, Gitea listener v1.27.3, Gitea MFA minimum 1.24, and Apache htpasswd 2.4; 2 further sources are pinned to a commit and 14 carry no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 62 claims, all reasoned from cited sources. -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): - -- portainer: Portainer (unknown) -- coolify: Coolify (unknown) -- dokploy: Dokploy (unknown) -- npm: Nginx Proxy Manager (unknown) -- vaultwarden: Vaultwarden docs (unknown) -- vaultwarden-pin: Vaultwarden config (3347698712d3e99e652ad4394ef2fc0ce2800fdd) -- dashboard: Dashboard docs (unknown) -- dashboard-old: Dashboard arguments (v2.7.0) -- jenkins: Jenkins (unknown) -- gitea: Gitea docs (unknown) -- gitlab: GitLab CE (unknown) -- kuma: Uptime Kuma (unknown) -- docker: Docker Engine (unknown) -- dozzle: Dozzle (unknown) -- filebrowser: Filebrowser (unknown) -- nodered: Node-RED (unknown) -- gitea-pin: Gitea listener (v1.27.3) -- gitea-mfa-min: Gitea MFA minimum (1.24) -- registry-docs: Docker Registry documentation (rolling) (unknown) -- htpasswd: Apache htpasswd (2.4) -- owasp-passwords: OWASP password storage (1a3f58ed2714f258a1d713966457f1a0f426e841) - -Claims (full source mapping in the front matter): - -- private (REASONED; portainer, docker): Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure. -- portainer-ports (REASONED; portainer): UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute. -- portainer-setup (REASONED; portainer): Setup requires the logged setup_token; first user is admin with a password of at least 12 characters. -- portainer-auth (REASONED; portainer): Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider. -- portainer-socket (REASONED; portainer, docker): Mounted docker.sock grants host-root power; treat a Portainer admin accordingly. -- coolify-setup (REASONED; coolify): HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately. -- coolify-tls (REASONED; coolify): Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close. -- coolify-firewall (REASONED; coolify): Docker bypasses UFW; restrict dashboard ports at the cloud firewall. -- coolify-ssh (REASONED; coolify): Server SSH keys require no passphrase; stored key material is the whole secret. -- dokploy-ports (REASONED; dokploy): UI is 3000, Traefik 80/443; first setup creates admin. -- dokploy-tls (REASONED; dokploy): Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000. -- npm-ports (REASONED; npm): Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in. -- npm-admin (REASONED; npm): Change default first-run admin credentials before other work. -- vaultwarden-tls (REASONED; vaultwarden, vaultwarden-pin): Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com. -- vaultwarden-signup (REASONED; vaultwarden, vaultwarden-pin): SIGNUPS_ALLOWED defaults true; set false to close registration. -- vaultwarden-invites (REASONED; vaultwarden, vaultwarden-pin): INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains. -- vaultwarden-admin (REASONED; vaultwarden, vaultwarden-pin): ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$. -- vaultwarden-session (REASONED; vaultwarden, vaultwarden-pin): Admin sessions expire after 20 minutes by default. -- vaultwarden-port (REASONED; vaultwarden-pin): Container port 80 or non-Docker 8000; publish only to loopback/proxy network. -- dashboard-lifecycle (REASONED; dashboard): Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp. -- dashboard-forward (REASONED; dashboard): Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine. -- dashboard-token (REASONED; dashboard): Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only. -- dashboard-old-flags (REASONED; dashboard-old): Older 2.x enable-skip-login and enable-insecure-login default false; keep both false. -- dashboard-mfa (REASONED; dashboard): API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI. -- jenkins-auth (REASONED; jenkins): Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too. -- jenkins-matrix (REASONED; jenkins): Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control. -- jenkins-csrf (REASONED; jenkins): Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch. -- jenkins-agents (REASONED; jenkins): Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port. -- jenkins-mfa (REASONED; jenkins): No native second factor; enforce MFA through SSO/fronting identity. -- gitea-bind (REASONED; gitea-pin): HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy. -- gitea-tls (REASONED; gitea): Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE. -- gitea-registration (REASONED; gitea): Set service DISABLE_REGISTRATION=true; default false. -- gitea-private (REASONED; gitea): Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false. -- gitea-mfa (REASONED; gitea, gitea-mfa-min): Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later. -- gitea-tokens (REASONED; gitea): With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens. -- gitlab-root (REASONED; gitlab): Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG. -- gitlab-signup (REASONED; gitlab): signup_enabled defaults true; disable for a private forge. -- gitlab-approval (REASONED; gitlab): New instances require admin approval after signup by default; do not rely on approval alone. -- gitlab-visibility (REASONED; gitlab): Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants. -- gitlab-mfa (REASONED; gitlab): Enforce two-factor authentication for every user; available on all tiers. -- kuma-proxy (REASONED; kuma): Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required. -- kuma-admin (REASONED; kuma): Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public. -- docker-root (REASONED; docker): Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375. -- docker-ssh (REASONED; docker): DOCKER_HOST/context can select SSH to the Unix socket without a new listener. -- docker-mtls (REASONED; docker): 2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too. -- dozzle-socket (REASONED; dozzle, docker): Docker-socket access has host-level power; keep Dozzle private with fronting MFA. -- dozzle-auth (REASONED; dozzle): Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy. -- dozzle-actions (REASONED; dozzle): Leave optional container actions/shell off unless needed; either enables remote command execution. -- registry-auth (REASONED; registry-docs): registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution. -- registry-bcrypt (REASONED; registry-docs, htpasswd, owasp-passwords): Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ. -- registry-private (REASONED; registry-docs): Use private networking and tunnels/Access with fronting MFA. -- filebrowser-admin (REASONED; filebrowser): Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure. -- filebrowser-private (REASONED; filebrowser): Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA. -- nodered-default (REASONED; nodered): Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification. -- nodered-auth (REASONED; nodered): Set adminAuth with bcrypt passwords from node-red admin hash-pw. -- nodered-secret (REASONED; nodered): Set credentialSecret yourself; otherwise generated, and stored credentials depend on it. -- nodered-private (REASONED; nodered): Keep editor private with fronting MFA; it has the power of its flows. -- verify-listeners (REASONED; portainer, dokploy, npm, kuma, docker): Read every panel/API listener; expect loopback or absence, never wildcard exposure. -- verify-panel (REASONED; portainer, jenkins, gitea): Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded. -- verify-docker-plain (REASONED; docker): Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive. -- verify-docker-mtls (REASONED; docker): 2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200. -- verify-gitlab (REASONED; gitlab): Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result. - +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 panels control hosts, containers, clusters, deploy keys, and secrets; a login to one of them is a login to everything it manages. One rule dominates everything tool-specific below: **a DevOps panel is never reachable from the public internet.** Bind it to loopback or a private interface and reach it through SSH port forwarding, a tailnet ([tailscale.md](tailscale.md)), or Cloudflare Access ([cloudflare.md](cloudflare.md)); anything that must be public sits behind a TLS proxy with its own authentication ([nginx.md](nginx.md), [caddy.md](caddy.md)) and the admin login gets MFA ([mfa.md](mfa.md)). Several of these tools create their administrator on first visit, so whoever reaches a fresh install first owns it: create the account before the port is reachable by anyone else. @@ -50200,6 +52820,363 @@ REASONED: preceding block; listener and panel-login checks, Docker 2375 refusal - Apache 2.4 `htpasswd` bcrypt default cost and `-C` override (checked October 2026): https://httpd.apache.org/docs/2.4/programs/htpasswd.html - OWASP password storage at commit 1a3f58ed2714f258a1d713966457f1a0f426e841 (bcrypt minimum work factor 10) (checked October 2026): https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139 + + +## 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/devops-uis.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): + +- portainer: Portainer (unknown) +- dashboard-old: Dashboard arguments (v2.7.0) +- gitea-pin: Gitea listener (v1.27.3) +- gitea-mfa-min: Gitea MFA minimum (1.24) +- coolify: Coolify (unknown) +- dokploy: Dokploy (unknown) +- npm: Nginx Proxy Manager (unknown) +- vaultwarden: Vaultwarden docs (unknown) +- vaultwarden-pin: Vaultwarden config (3347698712d3e99e652ad4394ef2fc0ce2800fdd) +- dashboard: Dashboard docs (unknown) +- jenkins: Jenkins (unknown) +- gitea: Gitea docs (unknown) +- kuma: Uptime Kuma (unknown) +- docker: Docker Engine (unknown) +- gitlab: GitLab CE (unknown) +- dozzle: Dozzle (unknown) +- filebrowser: Filebrowser (unknown) +- nodered: Node-RED (unknown) +- registry-docs: Docker Registry documentation (rolling) (unknown) +- htpasswd: Apache htpasswd (2.4) +- owasp-passwords: OWASP password storage (1a3f58ed2714f258a1d713966457f1a0f426e841) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; portainer, docker): Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure. +- portainer-ports (REASONED; portainer): UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute. +- portainer-setup (REASONED; portainer): Setup requires the logged setup_token; first user is admin with a password of at least 12 characters. +- portainer-auth (REASONED; portainer): Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider. +- portainer-socket (REASONED; portainer, docker): Mounted docker.sock grants host-root power; treat a Portainer admin accordingly. +- coolify-setup (REASONED; coolify): HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately. +- coolify-tls (REASONED; coolify): Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close. +- coolify-firewall (REASONED; coolify): Docker bypasses UFW; restrict dashboard ports at the cloud firewall. +- coolify-ssh (REASONED; coolify): Server SSH keys require no passphrase; stored key material is the whole secret. +- dokploy-ports (REASONED; dokploy): UI is 3000, Traefik 80/443; first setup creates admin. +- dokploy-tls (REASONED; dokploy): Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000. +- npm-ports (REASONED; npm): Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in. +- npm-admin (REASONED; npm): Change default first-run admin credentials before other work. +- vaultwarden-tls (REASONED; vaultwarden, vaultwarden-pin): Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com. +- vaultwarden-signup (REASONED; vaultwarden, vaultwarden-pin): SIGNUPS_ALLOWED defaults true; set false to close registration. +- vaultwarden-invites (REASONED; vaultwarden, vaultwarden-pin): INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains. +- vaultwarden-admin (REASONED; vaultwarden, vaultwarden-pin): ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$. +- vaultwarden-session (REASONED; vaultwarden, vaultwarden-pin): Admin sessions expire after 20 minutes by default. +- vaultwarden-port (REASONED; vaultwarden-pin): Container port 80 or non-Docker 8000; publish only to loopback/proxy network. +- dashboard-lifecycle (REASONED; dashboard): Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp. +- dashboard-forward (REASONED; dashboard): Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine. +- dashboard-token (REASONED; dashboard): Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only. +- dashboard-old-flags (REASONED; dashboard-old): Older 2.x enable-skip-login and enable-insecure-login default false; keep both false. +- dashboard-mfa (REASONED; dashboard): API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI. +- jenkins-auth (REASONED; jenkins): Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too. +- jenkins-matrix (REASONED; jenkins): Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control. +- jenkins-csrf (REASONED; jenkins): Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch. +- jenkins-agents (REASONED; jenkins): Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port. +- jenkins-mfa (REASONED; jenkins): No native second factor; enforce MFA through SSO/fronting identity. +- gitea-bind (REASONED; gitea-pin): HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy. +- gitea-tls (REASONED; gitea): Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE. +- gitea-registration (REASONED; gitea): Set service DISABLE_REGISTRATION=true; default false. +- gitea-private (REASONED; gitea): Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false. +- gitea-mfa (REASONED; gitea, gitea-mfa-min): Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later. +- gitea-tokens (REASONED; gitea): With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens. +- gitlab-root (REASONED; gitlab): Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG. +- gitlab-signup (REASONED; gitlab): signup_enabled defaults true; disable for a private forge. +- gitlab-approval (REASONED; gitlab): New instances require admin approval after signup by default; do not rely on approval alone. +- gitlab-visibility (REASONED; gitlab): Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants. +- gitlab-mfa (REASONED; gitlab): Enforce two-factor authentication for every user; available on all tiers. +- kuma-proxy (REASONED; kuma): Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required. +- kuma-admin (REASONED; kuma): Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public. +- docker-root (REASONED; docker): Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375. +- docker-ssh (REASONED; docker): DOCKER_HOST/context can select SSH to the Unix socket without a new listener. +- docker-mtls (REASONED; docker): 2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too. +- dozzle-socket (REASONED; dozzle, docker): Docker-socket access has host-level power; keep Dozzle private with fronting MFA. +- dozzle-auth (REASONED; dozzle): Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy. +- dozzle-actions (REASONED; dozzle): Leave optional container actions/shell off unless needed; either enables remote command execution. +- registry-auth (REASONED; registry-docs): registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution. +- registry-bcrypt (REASONED; registry-docs, htpasswd, owasp-passwords): Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ. +- registry-private (REASONED; registry-docs): Use private networking and tunnels/Access with fronting MFA. +- filebrowser-admin (REASONED; filebrowser): Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure. +- filebrowser-private (REASONED; filebrowser): Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA. +- nodered-default (REASONED; nodered): Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification. +- nodered-auth (REASONED; nodered): Set adminAuth with bcrypt passwords from node-red admin hash-pw. +- nodered-secret (REASONED; nodered): Set credentialSecret yourself; otherwise generated, and stored credentials depend on it. +- nodered-private (REASONED; nodered): Keep editor private with fronting MFA; it has the power of its flows. +- verify-listeners (REASONED; portainer, dokploy, npm, kuma, docker): Read every panel/API listener; expect loopback or absence, never wildcard exposure. +- verify-panel (REASONED; portainer, jenkins, gitea): Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded. +- verify-docker-plain (REASONED; docker): Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive. +- verify-docker-mtls (REASONED; docker): 2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200. +- verify-gitlab (REASONED; gitlab): Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "7cea8ce7dc2689ed90673f4d926b8259c0c8f0b24281b47ea184dfe91ffa1289", + "components": { + "portainer": { + "name": "Portainer", + "basis": "unknown", + "sources": { + "s3e163c7d3c38": "https://docs.portainer.io/start/install-ce/server/docker/linux", + "s115489d2f86f": "https://docs.portainer.io/start/install-ce/server/setup", + "s59297757a042": "https://docs.portainer.io/admin/settings/authentication", + "s8901bbe7c3d3": "https://docs.portainer.io/admin/settings/authentication/oauth" + } + }, + "dashboard-old": { + "name": "Dashboard arguments", + "basis": "v2.7.0", + "sources": { + "sacea2944651e": "https://github.com/kubernetes-retired/dashboard/blob/v2.7.0/docs/common/dashboard-arguments.md" + } + }, + "gitea-pin": { + "name": "Gitea listener", + "basis": "v1.27.3", + "sources": { + "s31dd622b1c12": "https://github.com/go-gitea/gitea/blob/v1.27.3/modules/setting/server.go#L121-L122" + } + }, + "gitea-mfa-min": { + "name": "Gitea MFA minimum", + "basis": "1.24", + "sources": { + "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet" + } + }, + "coolify": { + "name": "Coolify", + "basis": "unknown", + "sources": { + "s41e2d21202d9": "https://coolify.io/docs/start-with-self-hosted", + "sf2ae62425b35": "https://coolify.io/docs/core/infrastructure/servers/firewall", + "sf5d819d5d87d": "https://coolify.io/docs/core/networking/proxy/overview", + "s90addf9d8d5d": "https://coolify.io/docs/core/networking/dns", + "s683244a61c48": "https://coolify.io/docs/core/infrastructure/servers/openssh" + } + }, + "dokploy": { + "name": "Dokploy", + "basis": "unknown", + "sources": { + "sc2cc854c9b7e": "https://docs.dokploy.com/docs/core/installation" + } + }, + "npm": { + "name": "Nginx Proxy Manager", + "basis": "unknown", + "sources": { + "se553d384e783": "https://nginxproxymanager.com/setup/" + } + }, + "vaultwarden": { + "name": "Vaultwarden docs", + "basis": "unknown", + "sources": { + "s0bbe8ad856cd": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page", + "s6d3d0d02dcc9": "https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users", + "s60d064cae50e": "https://github.com/dani-garcia/vaultwarden/wiki/Enabling-HTTPS" + } + }, + "vaultwarden-pin": { + "name": "Vaultwarden config", + "basis": "3347698712d3e99e652ad4394ef2fc0ce2800fdd", + "sources": { + "s31c5fc79f5a0": "https://github.com/dani-garcia/vaultwarden/blob/3347698712d3e99e652ad4394ef2fc0ce2800fdd/.env.template" + } + }, + "dashboard": { + "name": "Dashboard docs", + "basis": "unknown", + "sources": { + "s73a36a43f6b5": "https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/" + } + }, + "jenkins": { + "name": "Jenkins", + "basis": "unknown", + "sources": { + "s524c8d5871f1": "https://www.jenkins.io/doc/book/security/managing-security/", + "sdfee751aeeee": "https://www.jenkins.io/doc/book/security/access-control/", + "s6f5287fd36d4": "https://www.jenkins.io/doc/book/security/csrf-protection/" + } + }, + "gitea": { + "name": "Gitea docs", + "basis": "unknown", + "sources": { + "s57933519c308": "https://docs.gitea.com/administration/config-cheat-sheet", + "s3b7060d58383": "https://docs.gitea.com/usage/user-setting/multi-factor-authentication/" + } + }, + "kuma": { + "name": "Uptime Kuma", + "basis": "unknown", + "sources": { + "s05b853ba300b": "https://github.com/louislam/uptime-kuma", + "sed52bfca4039": "https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" + } + }, + "docker": { + "name": "Docker Engine", + "basis": "unknown", + "sources": { + "sfe15a8156c18": "https://docs.docker.com/engine/security/protect-access/", + "s1b5ec54421b0": "https://docs.docker.com/engine/daemon/remote-access/" + } + }, + "gitlab": { + "name": "GitLab CE", + "basis": "unknown", + "sources": { + "s3b7ac561a92e": "https://docs.gitlab.com/install/docker/installation/", + "s6e8d16cad2ef": "https://docs.gitlab.com/administration/settings/sign_up_restrictions/", + "s6b8a68a77e17": "https://docs.gitlab.com/administration/settings/visibility_and_access_controls/", + "s0347014ce554": "https://docs.gitlab.com/security/two_factor_authentication/" + } + }, + "dozzle": { + "name": "Dozzle", + "basis": "unknown", + "sources": { + "s45b3eab2515b": "https://dozzle.dev/guide/authentication" + } + }, + "filebrowser": { + "name": "Filebrowser", + "basis": "unknown", + "sources": { + "s6386c9e2356b": "https://github.com/filebrowser/filebrowser" + } + }, + "nodered": { + "name": "Node-RED", + "basis": "unknown", + "sources": { + "sa94f329023fe": "https://nodered.org/docs/user-guide/runtime/securing-node-red" + } + }, + "registry-docs": { + "name": "Docker Registry documentation (rolling)", + "basis": "unknown", + "sources": { + "sa315b3f8bfa6": "https://distribution.github.io/distribution/about/deploying/" + } + }, + "htpasswd": { + "name": "Apache htpasswd", + "basis": "2.4", + "sources": { + "scde9004bb967": "https://httpd.apache.org/docs/2.4/programs/htpasswd.html" + } + }, + "owasp-passwords": { + "name": "OWASP password storage", + "basis": "1a3f58ed2714f258a1d713966457f1a0f426e841", + "sources": { + "s0f70ec4aa271": "https://github.com/OWASP/CheatSheetSeries/blob/1a3f58ed2714f258a1d713966457f1a0f426e841/cheatsheets/Password_Storage_Cheat_Sheet.md#L139" + } + } + }, + "claims": { + "private": {"text": "Keep panels private; public front doors need proxy TLS/auth and MFA. Complete first-run administration before exposure.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, + "portainer-ports": {"text": "UI HTTPS 9443 is self-signed by default; supply a certificate/proxy. Leave legacy HTTP 9000 unpublished; publish tunnel 8000 only for Edge Compute.", "components": ["portainer"], "sources": ["portainer:s3e163c7d3c38"], "status": "REASONED"}, + "portainer-setup": {"text": "Setup requires the logged setup_token; first user is admin with a password of at least 12 characters.", "components": ["portainer"], "sources": ["portainer:s115489d2f86f"], "status": "REASONED"}, + "portainer-auth": {"text": "Internal, LDAP, AD or OAuth login; no documented native second factor. Enforce MFA at the OAuth provider.", "components": ["portainer"], "sources": ["portainer:s59297757a042", "portainer:s8901bbe7c3d3"], "status": "REASONED"}, + "portainer-socket": {"text": "Mounted docker.sock grants host-root power; treat a Portainer admin accordingly.", "components": ["portainer", "docker"], "sources": ["portainer:s3e163c7d3c38", "docker:sfe15a8156c18"], "status": "REASONED"}, + "coolify-setup": {"text": "HTTP dashboard starts on 8000; first registration claims server control, so create admin immediately.", "components": ["coolify"], "sources": ["coolify:s41e2d21202d9"], "status": "REASONED"}, + "coolify-tls": {"text": "Custom domain in /settings lets Traefik/Caddy issue/renew TLS; dashboard ports 8000/6001/6002 can then close.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35", "coolify:sf5d819d5d87d", "coolify:s90addf9d8d5d"], "status": "REASONED"}, + "coolify-firewall": {"text": "Docker bypasses UFW; restrict dashboard ports at the cloud firewall.", "components": ["coolify"], "sources": ["coolify:sf2ae62425b35"], "status": "REASONED"}, + "coolify-ssh": {"text": "Server SSH keys require no passphrase; stored key material is the whole secret.", "components": ["coolify"], "sources": ["coolify:s683244a61c48"], "status": "REASONED"}, + "dokploy-ports": {"text": "UI is 3000, Traefik 80/443; first setup creates admin.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, + "dokploy-tls": {"text": "Configure a panel domain and Let's Encrypt/custom certificate, then remove published 3000.", "components": ["dokploy"], "sources": ["dokploy:sc2cc854c9b7e"], "status": "REASONED"}, + "npm-ports": {"text": "Admin is 81, proxy 80/443; publish 127.0.0.1:81:81 and tunnel in.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, + "npm-admin": {"text": "Change default first-run admin credentials before other work.", "components": ["npm"], "sources": ["npm:se553d384e783"], "status": "REASONED"}, + "vaultwarden-tls": {"text": "Web-vault crypto needs HTTPS; prefer proxy TLS over ROCKET_TLS and set DOMAIN=https://vault.example.com.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s60d064cae50e", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-signup": {"text": "SIGNUPS_ALLOWED defaults true; set false to close registration.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-invites": {"text": "INVITATIONS_ALLOWED=true still permits owner/admin invitations; SIGNUPS_DOMAINS_WHITELIST admits selected domains.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s6d3d0d02dcc9", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-admin": {"text": "ADMIN_TOKEN enables the disabled admin page; enable HTTPS first, use vaultwarden hash argon2id PHC, and escape Compose dollars as $$.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-session": {"text": "Admin sessions expire after 20 minutes by default.", "components": ["vaultwarden", "vaultwarden-pin"], "sources": ["vaultwarden:s0bbe8ad856cd", "vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "vaultwarden-port": {"text": "Container port 80 or non-Docker 8000; publish only to loopback/proxy network.", "components": ["vaultwarden-pin"], "sources": ["vaultwarden-pin:s31c5fc79f5a0"], "status": "REASONED"}, + "dashboard-lifecycle": {"text": "Recorded September 2026: deprecated/unmaintained, archived January 2026; Kubernetes points new installs to Headlamp.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-forward": {"text": "Avoid public LoadBalancer/Ingress; port-forward kong-proxy 8443:443 and use https://localhost:8443 on the operator machine.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-token": {"text": "Use minimally privileged ServiceAccount bearer tokens; sample cluster-admin is demonstration-only.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "dashboard-old-flags": {"text": "Older 2.x enable-skip-login and enable-insecure-login default false; keep both false.", "components": ["dashboard-old"], "sources": ["dashboard-old:sacea2944651e"], "status": "REASONED"}, + "dashboard-mfa": {"text": "API-server OIDC gives ServiceAccount tokens no MFA; enforce human MFA on the access path or an IdP-aware UI.", "components": ["dashboard"], "sources": ["dashboard:s73a36a43f6b5"], "status": "REASONED"}, + "jenkins-auth": {"text": "Realm/authorization are separate; setup creates one local admin. Disable signup because strategy grants apply to new users too.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, + "jenkins-matrix": {"text": "Use Matrix Authorization; grant nothing significant to anonymous/authenticated. Anonymous Overall/Administer is full control.", "components": ["jenkins"], "sources": ["jenkins:sdfee751aeeee"], "status": "REASONED"}, + "jenkins-csrf": {"text": "Keep CSRF enabled even privately; disabling uses hudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION, not a UI switch.", "components": ["jenkins"], "sources": ["jenkins:s6f5287fd36d4"], "status": "REASONED"}, + "jenkins-agents": {"text": "Inbound agents use fixed/random TCP or WebSocket on HTTPS; prefer WebSocket or isolate the extra port.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1"], "status": "REASONED"}, + "jenkins-mfa": {"text": "No native second factor; enforce MFA through SSO/fronting identity.", "components": ["jenkins"], "sources": ["jenkins:s524c8d5871f1", "jenkins:sdfee751aeeee"], "status": "REASONED"}, + "gitea-bind": {"text": "HTTP_ADDR=0.0.0.0 and HTTP_PORT=3000 default; set 127.0.0.1 behind a proxy.", "components": ["gitea-pin"], "sources": ["gitea-pin:s31dd622b1c12"], "status": "REASONED"}, + "gitea-tls": {"text": "Native TLS uses PROTOCOL=https, CERT_FILE and KEY_FILE.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-registration": {"text": "Set service DISABLE_REGISTRATION=true; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-private": {"text": "Set service REQUIRE_SIGNIN_VIEW=true for a private forge; default false.", "components": ["gitea"], "sources": ["gitea:s57933519c308"], "status": "REASONED"}, + "gitea-mfa": {"text": "Enrol TOTP/WebAuthn; security TWO_FACTOR_AUTH=enforced requires MFA in Gitea 1.24 and later.", "components": ["gitea", "gitea-mfa-min"], "sources": ["gitea:s57933519c308", "gitea:s3b7060d58383", "gitea-mfa-min:s57933519c308"], "status": "REASONED"}, + "gitea-tokens": {"text": "With MFA, Git HTTP uses tokens that bypass MFA; scope and revoke unused tokens.", "components": ["gitea"], "sources": ["gitea:s3b7060d58383"], "status": "REASONED"}, + "gitlab-root": {"text": "Rotate generated initial_root_password; file deletion is on first restart after 24 hours. Alternatively preset through gitlab.rb/OMNIBUS_CONFIG.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED"}, + "gitlab-signup": {"text": "signup_enabled defaults true; disable for a private forge.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, + "gitlab-approval": {"text": "New instances require admin approval after signup by default; do not rely on approval alone.", "components": ["gitlab"], "sources": ["gitlab:s6e8d16cad2ef"], "status": "REASONED"}, + "gitlab-visibility": {"text": "Project/group visibility default private; internal admits every signed-in non-external user, including allowed registrants.", "components": ["gitlab"], "sources": ["gitlab:s6b8a68a77e17"], "status": "REASONED"}, + "gitlab-mfa": {"text": "Enforce two-factor authentication for every user; available on all tiers.", "components": ["gitlab"], "sources": ["gitlab:s0347014ce554"], "status": "REASONED"}, + "kuma-proxy": {"text": "Port 3001 uses WebSockets; proxy Upgrade/Connection headers are required.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b", "kuma:sed52bfca4039"], "status": "REASONED"}, + "kuma-admin": {"text": "Finish account setup before others, enable 2FA and keep dashboard private even if status pages are public.", "components": ["kuma"], "sources": ["kuma:s05b853ba300b"], "status": "REASONED"}, + "docker-root": {"text": "Docker socket grants host-root power; never expose unauthenticated 0.0.0.0:2375.", "components": ["docker"], "sources": ["docker:sfe15a8156c18", "docker:s1b5ec54421b0"], "status": "REASONED"}, + "docker-ssh": {"text": "DOCKER_HOST/context can select SSH to the Unix socket without a new listener.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, + "docker-mtls": {"text": "2376 requires tlsverify, CA, server cert/key and client TLS settings. Without tlsverify clients are not authenticated; firewall source addresses too.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED"}, + "dozzle-socket": {"text": "Docker-socket access has host-level power; keep Dozzle private with fronting MFA.", "components": ["dozzle", "docker"], "sources": ["dozzle:s45b3eab2515b", "docker:sfe15a8156c18"], "status": "REASONED"}, + "dozzle-auth": {"text": "Auth defaults off; generate users.yml with stdin password prompt and set simple auth, or delegate with forward-proxy.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, + "dozzle-actions": {"text": "Leave optional container actions/shell off unless needed; either enables remote command execution.", "components": ["dozzle"], "sources": ["dozzle:s45b3eab2515b"], "status": "REASONED"}, + "registry-auth": {"text": "registry:2 defaults to unauthenticated push/pull; configure TLS before htpasswd/token auth or use an authenticated distribution.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, + "registry-bcrypt": {"text": "Native htpasswd accepts bcrypt only; use -B -C 12. Proxy hashing rules differ.", "components": ["registry-docs", "htpasswd", "owasp-passwords"], "sources": ["registry-docs:sa315b3f8bfa6", "htpasswd:scde9004bb967", "owasp-passwords:s0f70ec4aa271"], "status": "REASONED"}, + "registry-private": {"text": "Use private networking and tunnels/Access with fronting MFA.", "components": ["registry-docs"], "sources": ["registry-docs:sa315b3f8bfa6"], "status": "REASONED"}, + "filebrowser-admin": {"text": "Change first-run admin credentials, historically admin/admin. Guide records September 1, 2026 archival and the warning against internet exposure.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, + "filebrowser-private": {"text": "Keep Filebrowser private through tunnel/tailnet/Access with fronting MFA.", "components": ["filebrowser"], "sources": ["filebrowser:s6386c9e2356b"], "status": "REASONED"}, + "nodered-default": {"text": "Editor/admin API on 1880 defaults unauthenticated, permitting flow viewing/deployment/modification.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-auth": {"text": "Set adminAuth with bcrypt passwords from node-red admin hash-pw.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-secret": {"text": "Set credentialSecret yourself; otherwise generated, and stored credentials depend on it.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "nodered-private": {"text": "Keep editor private with fronting MFA; it has the power of its flows.", "components": ["nodered"], "sources": ["nodered:sa94f329023fe"], "status": "REASONED"}, + "verify-listeners": {"text": "Read every panel/API listener; expect loopback or absence, never wildcard exposure.", "components": ["portainer", "dokploy", "npm", "kuma", "docker"], "sources": ["portainer:s3e163c7d3c38", "dokploy:sc2cc854c9b7e", "npm:se553d384e783", "kuma:s05b853ba300b", "docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, + "verify-panel": {"text": "Expect denial/login redirect with valid TLS; anonymous host/container/repository data is a finding. No live panel outcome recorded.", "components": ["portainer", "jenkins", "gitea"], "sources": ["portainer:s3e163c7d3c38", "jenkins:sdfee751aeeee", "gitea:s57933519c308"], "status": "REASONED", "verify": [1]}, + "verify-docker-plain": {"text": "Public-IP 2375 must be refused/filtered by the remote host; local client failure is inconclusive.", "components": ["docker"], "sources": ["docker:s1b5ec54421b0"], "status": "REASONED", "verify": [1]}, + "verify-docker-mtls": {"text": "2376 /_ping without a client certificate must fail client-certificate authentication; correct certificate/key must return 200.", "components": ["docker"], "sources": ["docker:sfe15a8156c18"], "status": "REASONED", "verify": [1]}, + "verify-gitlab": {"text": "Present initial_root_password requires rotation; absence does not prove rotation. Failed/unexpected docker exec output is not a result.", "components": ["gitlab"], "sources": ["gitlab:s3b7ac561a92e"], "status": "REASONED", "verify": [1]} + } +} +```` + + ====================================================================== ==> cors.md ====================================================================== @@ -52309,59 +55286,9 @@ Claims (full source mapping in the JSON block below): # Files a web server must never serve: dotfiles, .git, dumps, backups, and client secrets - -**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): - -- nginx: nginx documentation (unknown) -- apache: Apache documentation (2.4) -- caddy: Caddy documentation (v2.11.4) -- next: Next.js documentation (unknown) -- vite: Vite documentation (unknown) -- cra: Create React App documentation (unknown) -- coreutils: GNU coreutils documentation (unknown) -- grep: GNU grep (3.12) -- caddy-rolling: Caddy documentation (rolling) (unknown) -- acme-http01: ACME HTTP-01 (RFC 8555) -- debian-packaging: Debian apache2 packaging (2.4.69-1) -- ecma426: ECMA-426 (c724f0f494b44b21b651fdbd6412d3437eef0e72) -- next-maps: Next.js browser source maps (v16.3.6) -- vite-maps: Vite production source maps (v8.3.1) - -Claims (full source mapping in the front matter): +Written against nginx documentation (no recorded version), Apache documentation 2.4, Caddy documentation v2.11.4, Next.js browser source maps v16.3.6, Vite production source maps v8.3.1, Debian apache2 packaging 2.4.69-1, ACME HTTP-01 RFC 8555, and GNU grep 3.12; 1 further source is pinned to a commit and 5 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 26 claims, all reasoned from cited sources. -- nginx-dotfiles (REASONED; nginx): Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them. -- nginx-backups (REASONED; nginx): Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence. -- nginx-acme (REASONED; nginx): Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets. -- nginx-security-txt (REASONED; nginx): The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations. -- apache-directories (REASONED; apache): DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/. -- apache-files (REASONED; apache): Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config. -- apache-listing (REASONED; apache, debian-packaging): Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide. -- apache-forbidden (REASONED; apache): mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed. -- apache-options (REASONED; apache): Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options. -- nginx-listing (REASONED; nginx): nginx autoindex defaults off. -- caddy-listing (REASONED; caddy-rolling, caddy): Guide says Caddy lists only with file_server browse. -- caddy-deny (REASONED; caddy, caddy-rolling): Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run. -- caddy-acme (REASONED; caddy, caddy-rolling): ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets. -- backup-storage (REASONED; nginx, apache, caddy, caddy-rolling): Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names. -- suffix-policy (REASONED; nginx, apache, caddy, caddy-rolling): Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible. -- next-public (REASONED; next): NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes. -- vite-public (REASONED; vite): VITE_ embeds values in browser JavaScript; reserve for public values. -- cra-public (REASONED; cra): Deprecated Create React App embeds REACT_APP_ values in browser builds. -- source-maps (REASONED; next, vite, ecma426, next-maps, vite-maps): ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public. -- verify-dotfiles (REASONED; nginx, apache, caddy, caddy-rolling): Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes. -- verify-listing (REASONED; apache): Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward. -- verify-acme (REASONED; nginx, apache, acme-http01): Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive. -- verify-lookalike (REASONED; nginx, apache, caddy, caddy-rolling): Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it. -- verify-bundle (REASONED; next, vite, cra, grep): Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets. -- verify-backup (REASONED; nginx, apache, caddy, caddy-rolling): Known previously deployed backup URLs should return 403 or 404 without the file contents. -- verify-suffixes (REASONED; nginx, apache, caddy, coreutils, caddy-rolling): GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution. - +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. Scanners request `/.env`, `/.git/config`, `/config.php.bak`, and `/db.sql` continuously, and any of these under a web root hands over credentials or source regardless of what the application itself authenticates. @@ -52609,6 +55536,238 @@ done - Next.js v16.3.6 production browser source maps default and opt-in serving (checked October 2026): https://github.com/vercel/next.js/blob/v16.3.6/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18 - Vite v8.3.1 build.sourcemap default and output modes (checked October 2026): https://github.com/vitejs/vite/blob/v8.3.1/docs/config/build-options.md#L145-L150 + + +## 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/web-exposure.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: nginx documentation (unknown) +- apache: Apache documentation (2.4) +- caddy: Caddy documentation (v2.11.4) +- caddy-rolling: Caddy documentation (rolling) (unknown) +- next: Next.js documentation (unknown) +- vite: Vite documentation (unknown) +- cra: Create React App documentation (unknown) +- next-maps: Next.js browser source maps (v16.3.6) +- vite-maps: Vite production source maps (v8.3.1) +- debian-packaging: Debian apache2 packaging (2.4.69-1) +- acme-http01: ACME HTTP-01 (RFC 8555) +- ecma426: ECMA-426 (c724f0f494b44b21b651fdbd6412d3437eef0e72) +- coreutils: GNU coreutils documentation (unknown) +- grep: GNU grep (3.12) + +Claims (full source mapping in the JSON block below): + +- nginx-dotfiles (REASONED; nginx): Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them. +- nginx-backups (REASONED; nginx): Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence. +- nginx-acme (REASONED; nginx): Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets. +- nginx-security-txt (REASONED; nginx): The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations. +- apache-directories (REASONED; apache): DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/. +- apache-files (REASONED; apache): Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config. +- apache-listing (REASONED; apache, debian-packaging): Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide. +- apache-forbidden (REASONED; apache): mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed. +- apache-options (REASONED; apache): Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options. +- nginx-listing (REASONED; nginx): nginx autoindex defaults off. +- caddy-listing (REASONED; caddy-rolling, caddy): Guide says Caddy lists only with file_server browse. +- caddy-deny (REASONED; caddy, caddy-rolling): Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run. +- caddy-acme (REASONED; caddy, caddy-rolling): ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets. +- backup-storage (REASONED; nginx, apache, caddy, caddy-rolling): Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names. +- suffix-policy (REASONED; nginx, apache, caddy, caddy-rolling): Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible. +- next-public (REASONED; next): NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes. +- vite-public (REASONED; vite): VITE_ embeds values in browser JavaScript; reserve for public values. +- cra-public (REASONED; cra): Deprecated Create React App embeds REACT_APP_ values in browser builds. +- source-maps (REASONED; next, vite, ecma426, next-maps, vite-maps): ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public. +- verify-dotfiles (REASONED; nginx, apache, caddy, caddy-rolling): Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes. +- verify-listing (REASONED; apache): Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward. +- verify-acme (REASONED; nginx, apache, acme-http01): Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive. +- verify-lookalike (REASONED; nginx, apache, caddy, caddy-rolling): Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it. +- verify-bundle (REASONED; next, vite, cra, grep): Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets. +- verify-backup (REASONED; nginx, apache, caddy, caddy-rolling): Known previously deployed backup URLs should return 403 or 404 without the file contents. +- verify-suffixes (REASONED; nginx, apache, caddy, coreutils, caddy-rolling): GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "d04f9d877201565cfdd6fe952b50f26d3d85bd16e74efce72a6ef79cebdee43b", + "components": { + "nginx": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s0e5162d1ce2b": "https://nginx.org/en/docs/http/ngx_http_access_module.html", + "sbd17bad53c9d": "https://nginx.org/en/docs/http/ngx_http_autoindex_module.html", + "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html" + } + }, + "apache": { + "name": "Apache documentation", + "basis": "2.4", + "sources": { + "s24f06763d14a": "https://httpd.apache.org/docs/2.4/mod/core.html#directorymatch", + "se24e9bbc0443": "https://httpd.apache.org/docs/2.4/mod/core.html#filesmatch", + "s97468bf1f299": "https://httpd.apache.org/docs/2.4/mod/core.html#options", + "s7313b1b624d5": "https://httpd.apache.org/docs/2.4/mod/mod_authz_core.html", + "sef5ff46eddf7": "https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html" + } + }, + "caddy": { + "name": "Caddy documentation", + "basis": "v2.11.4", + "sources": { + "s450aa9ed710b": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/matchers.go", + "se8dd8140d6b8": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/fileserver/staticfiles.go#L140-L142", + "s86912a2959e0": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/fileserver/staticfiles.go#L371-L374", + "s128797d9a40f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/server.go#L384-L388" + } + }, + "caddy-rolling": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "se9ab71afc4ba": "https://caddyserver.com/docs/caddyfile/directives/respond", + "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" + } + }, + "next": { + "name": "Next.js documentation", + "basis": "unknown", + "sources": { + "sb98bee5b61c8": "https://nextjs.org/docs/pages/guides/environment-variables" + } + }, + "vite": { + "name": "Vite documentation", + "basis": "unknown", + "sources": { + "s3fc0892e0f58": "https://vite.dev/guide/env-and-mode" + } + }, + "cra": { + "name": "Create React App documentation", + "basis": "unknown", + "sources": { + "scfb24624addd": "https://create-react-app.dev/docs/adding-custom-environment-variables/" + } + }, + "next-maps": { + "name": "Next.js browser source maps", + "basis": "v16.3.6", + "sources": { + "s6964da09fc21": "https://github.com/vercel/next.js/blob/v16.3.6/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" + } + }, + "vite-maps": { + "name": "Vite production source maps", + "basis": "v8.3.1", + "sources": { + "sf12c9114bd79": "https://github.com/vitejs/vite/blob/v8.3.1/docs/config/build-options.md#L145-L150" + } + }, + "debian-packaging": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1", + "s90898e0f03b6": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/apache2.conf.in#L170-L174", + "s6f77b580cc87": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/mods-available/dir.conf#L1", + "s7ea1fcaca182": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.postinst#L38-L52", + "sd7d93d2a9705": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.postinst#L257-L266" + } + }, + "acme-http01": { + "name": "ACME HTTP-01", + "basis": "RFC 8555", + "sources": { + "se60bdd68e41d": "https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3" + } + }, + "ecma426": { + "name": "ECMA-426", + "basis": "c724f0f494b44b21b651fdbd6412d3437eef0e72", + "sources": { + "s5de29cfb3e99": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L584-L585", + "s2af3a1c52b4a": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1294", + "sfeb03b2200f4": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1323", + "s94b6d647e4bc": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1415" + } + }, + "coreutils": { + "name": "GNU coreutils documentation", + "basis": "unknown", + "sources": { + "sabbd081d1d5a": "https://www.gnu.org/s/coreutils/manual/html_node/mktemp-invocation.html" + } + }, + "grep": { + "name": "GNU grep", + "basis": "3.12", + "sources": { + "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", + "s057470125f4c": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n425", + "sb18b36e247d7": "https://git.savannah.gnu.org/cgit/grep.git/tree/src/grep.c?h=v3.12#n1756", + "s0c4ddbee9e68": "https://git.savannah.gnu.org/cgit/grep.git/tree/src/grep.c?h=v3.12#n661" + } + } + }, + "claims": { + "nginx-dotfiles": {"text": "Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-backups": {"text": "Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-acme": {"text": "Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-security-txt": {"text": "The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "apache-directories": {"text": "DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/.", "components": ["apache"], "sources": ["apache:s24f06763d14a", "apache:s7313b1b624d5", "apache:se24e9bbc0443"], "status": "REASONED"}, + "apache-files": {"text": "Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config.", "components": ["apache"], "sources": ["apache:se24e9bbc0443", "apache:s7313b1b624d5"], "status": "REASONED"}, + "apache-listing": {"text": "Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide.", "components": ["apache", "debian-packaging"], "sources": ["apache:s97468bf1f299", "apache:sef5ff46eddf7", "debian-packaging:s62f6ed2866ab", "debian-packaging:s90898e0f03b6", "debian-packaging:s6f77b580cc87", "debian-packaging:s7ea1fcaca182", "debian-packaging:sd7d93d2a9705"], "status": "REASONED"}, + "apache-forbidden": {"text": "mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed.", "components": ["apache"], "sources": ["apache:sef5ff46eddf7"], "status": "REASONED"}, + "apache-options": {"text": "Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options.", "components": ["apache"], "sources": ["apache:s97468bf1f299"], "status": "REASONED"}, + "nginx-listing": {"text": "nginx autoindex defaults off.", "components": ["nginx"], "sources": ["nginx:sbd17bad53c9d"], "status": "REASONED"}, + "caddy-listing": {"text": "Guide says Caddy lists only with file_server browse.", "components": ["caddy-rolling", "caddy"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:se8dd8140d6b8", "caddy:s86912a2959e0"], "status": "REASONED"}, + "caddy-deny": {"text": "Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run.", "components": ["caddy", "caddy-rolling"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "caddy-acme": {"text": "ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets.", "components": ["caddy", "caddy-rolling"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b", "caddy:s128797d9a40f"], "status": "REASONED"}, + "backup-storage": {"text": "Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "suffix-policy": {"text": "Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "next-public": {"text": "NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes.", "components": ["next"], "sources": ["next:sb98bee5b61c8"], "status": "REASONED"}, + "vite-public": {"text": "VITE_ embeds values in browser JavaScript; reserve for public values.", "components": ["vite"], "sources": ["vite:s3fc0892e0f58"], "status": "REASONED"}, + "cra-public": {"text": "Deprecated Create React App embeds REACT_APP_ values in browser builds.", "components": ["cra"], "sources": ["cra:scfb24624addd"], "status": "REASONED"}, + "source-maps": {"text": "ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public.", "components": ["next", "vite", "ecma426", "next-maps", "vite-maps"], "sources": ["next:sb98bee5b61c8", "vite:s3fc0892e0f58", "ecma426:s5de29cfb3e99", "ecma426:s2af3a1c52b4a", "ecma426:sfeb03b2200f4", "ecma426:s94b6d647e4bc", "next-maps:s6964da09fc21", "vite-maps:sf12c9114bd79"], "status": "REASONED"}, + "verify-dotfiles": {"text": "Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:s24f06763d14a", "apache:s7313b1b624d5", "apache:se24e9bbc0443", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED", "verify": [1]}, + "verify-listing": {"text": "Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward.", "components": ["apache"], "sources": ["apache:s97468bf1f299", "apache:sef5ff46eddf7"], "status": "REASONED", "verify": [1]}, + "verify-acme": {"text": "Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive.", "components": ["nginx", "apache", "acme-http01"], "sources": ["nginx:s40bdf1af1596", "nginx:s0e5162d1ce2b", "apache:s24f06763d14a", "acme-http01:se60bdd68e41d"], "status": "REASONED", "verify": [1]}, + "verify-lookalike": {"text": "Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:s24f06763d14a", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED", "verify": [1]}, + "verify-bundle": {"text": "Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets.", "components": ["next", "vite", "cra", "grep"], "sources": ["next:sb98bee5b61c8", "vite:s3fc0892e0f58", "cra:scfb24624addd", "grep:s5170317ddea8", "grep:s057470125f4c", "grep:sb18b36e247d7", "grep:s0c4ddbee9e68"], "status": "REASONED", "verify": [1]}, + "verify-backup": {"text": "Known previously deployed backup URLs should return 403 or 404 without the file contents.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "verify-suffixes": {"text": "GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution.", "components": ["nginx", "apache", "caddy", "coreutils", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b", "coreutils:sabbd081d1d5a"], "status": "REASONED", "verify": [2]} + } +} +```` + + ====================================================================== ==> realtime-webhooks.md ====================================================================== @@ -53135,66 +56294,9 @@ Claims (full source mapping in the JSON block below): # Transactional email posture: authenticating the mail your deployment sends - -**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): - -- spf: SPF (RFC 7208) -- dkim: DKIM (RFC 6376) -- crypto: DKIM cryptography (RFC 8301) -- dmarc: DMARC (RFC 9989) -- aggregate: DMARC aggregate reporting (RFC 9990) -- failure: DMARC failure reporting (RFC 9991) -- submission: Submission TLS (RFC 8314) -- roles: SMTP submission roles (RFC 6409) -- relay: SMTP relay controls (RFC 2505) -- sts: MTA-STS (RFC 8461) -- rpt: TLS-RPT (RFC 8460) -- results: Authentication-Results (RFC 8601) -- bimi: BIMI documentation (unknown) -- submission-update: Submission TLS update (RFC 8997) -- bind: BIND dig manual (9.18.39) -- openssl: OpenSSL command documentation (3.0) - -Claims (full source mapping in the front matter): +Written against SPF RFC 7208, DKIM RFC 6376, DKIM cryptography RFC 8301, DMARC RFC 9989, DMARC aggregate reporting RFC 9990, DMARC failure reporting RFC 9991, Submission TLS RFC 8314, and SMTP submission roles RFC 6409, plus 7 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 31 claims, all reasoned from cited sources. -- spf-identity (REASONED; spf, dmarc): SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment. -- spf-all (REASONED; spf): -all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone. -- spf-record (REASONED; spf): Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror. -- spf-lookups (REASONED; spf): Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass. -- dkim-signing (REASONED; dkim, dmarc): DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC. -- dkim-crypto (REASONED; crypto): Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example. -- dkim-rotation (REASONED; dkim): Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership. -- dmarc-alignment (REASONED; dmarc): DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching. -- dmarc-policy (REASONED; dmarc): p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery. -- dmarc-subdomains (REASONED; dmarc): sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies. -- dmarc-discovery (REASONED; dmarc): RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method. -- dmarc-testing (REASONED; dmarc): t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag. -- dmarc-rollout (REASONED; dmarc, aggregate): Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected. -- dmarc-pct (REASONED; dmarc): RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control. -- aggregate (REASONED; aggregate): Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked. -- failure-reports (REASONED; failure): ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations. -- starttls (REASONED; submission): 587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS. -- implicit-tls (REASONED; submission): 465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward. -- tls-validation (REASONED; submission, submission-update): Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997. -- port-25 (REASONED; roles): 25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication. -- relay-policy (REASONED; relay): Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths. -- submission-auth (REASONED; roles, submission): After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds. -- sts-publication (REASONED; sts): MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800. -- sts-enforcement (REASONED; sts): Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail. -- sts-refresh (REASONED; sts): Update the MTA-STS TXT id whenever the policy changes. -- tls-reports (REASONED; rpt): TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement. -- bimi (REASONED; bimi): BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last. -- verify-dns (REASONED; spf, dkim, dmarc, sts, rpt, bimi, bind): Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available. -- verify-tls (REASONED; submission, submission-update, openssl): 587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available. -- verify-alignment (REASONED; results, dmarc): Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling. -- verify-relay (REASONED; relay, roles): In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved. - +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. Password resets, sign-in links, and invitations are part of your authentication path: a recipient who trusts a forged one hands an attacker the account. The controls that stop that forgery are not a single service's settings but three DNS-published standards (SPF, DKIM, and DMARC), the submission channel your application sends through, and the transport policy between mail servers. This guide covers that posture for a deployment that sends transactional mail. It is standards-based rather than product-based, so [authentication.md](authentication.md) covers the account side and [secrets.md](secrets.md) covers the SMTP credential and DKIM private key you will handle here. @@ -53342,61 +56444,255 @@ REASONED: preceding block; DNS/TLS inspection, message alignment and relay discr - OpenSSL 3.0 `s_client` (checked October 2026; SMTP STARTTLS, TLS connections, hostname verification, and verification errors): https://docs.openssl.org/3.0/man1/openssl-s_client/ - OpenSSL 3.0 protocol configuration (checked October 2026; `-min_protocol TLSv1.2`): https://docs.openssl.org/3.0/man3/SSL_CONF_cmd/ -====================================================================== -==> bi-dashboards.md -====================================================================== - -# BI dashboards: Metabase, Superset, Redash +## 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/transactional-email-posture.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- metabase-docs: Metabase docs (unknown) -- metabase: Metabase (v0.63.18) -- environ: environ (1.2.0) -- ring: Ring Jetty adapter (1.15.3) -- jetty: Jetty (12.1.13) -- superset: Superset docs (unknown) -- superset-compose: Superset Compose (3d01094d231712e3470eb625be3f9925aeaeadb5) -- redash-docs: Redash docs (unknown) -- redash: Redash (v26.9.0) -- docker: Docker publishing (unknown) +- spf: SPF (RFC 7208) +- dkim: DKIM (RFC 6376) +- crypto: DKIM cryptography (RFC 8301) +- dmarc: DMARC (RFC 9989) +- aggregate: DMARC aggregate reporting (RFC 9990) +- failure: DMARC failure reporting (RFC 9991) +- submission: Submission TLS (RFC 8314) +- roles: SMTP submission roles (RFC 6409) +- relay: SMTP relay controls (RFC 2505) +- sts: MTA-STS (RFC 8461) +- rpt: TLS-RPT (RFC 8460) +- results: Authentication-Results (RFC 8601) +- bimi: BIMI documentation (unknown) +- submission-update: Submission TLS update (RFC 8997) +- bind: BIND dig manual (9.18.39) +- openssl: OpenSSL command documentation (3.0) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- private (REASONED; redash-docs, metabase-docs): Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed. -- metabase-setup (REASONED; metabase-docs): First account is admin; finish setup before anyone else can reach it. -- metabase-precedence (REASONED; metabase, environ): Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings. -- metabase-bind (REASONED; metabase, ring, jetty): Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy. -- metabase-image (REASONED; metabase, docker): Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start. -- metabase-port (REASONED; metabase): Port defaults 3000 unless the final MB_JETTY_PORT is nonblank. -- metabase-sharing (REASONED; metabase-docs): Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing. -- metabase-embed (REASONED; metabase-docs): Public embed URLs are recoverable; embeds do not strengthen the public-link boundary. -- metabase-sandbox (REASONED; metabase-docs): Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries. -- superset-secret (REASONED; superset): Set a unique complex random SECRET_KEY before exposure; it signs session cookies. -- superset-admin (REASONED; superset, superset-compose): Replace the first-run admin password before exposure. -- superset-public (REASONED; superset): Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default. -- superset-copy (REASONED; superset): PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards. -- superset-compose (REASONED; superset-compose): Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data. -- redash-setup (REASONED; redash-docs): Create the first-run admin before others reach setup. -- redash-https (REASONED; redash-docs): Enforce HTTPS and the private-access pattern. -- redash-cookie (REASONED; redash-docs): Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it. -- redash-encryption (REASONED; redash-docs): REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control. -- redash-bind (REASONED; redash): Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately. -- verify-credentials (REASONED; metabase-docs, superset-compose, redash-docs): Confirm no default admin/admin or example credentials work. -- verify-db (REASONED; metabase-docs): Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately. -- verify-listeners (REASONED; metabase, superset-compose, redash, docker): Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation. -- verify-ports (REASONED; metabase, superset-compose, redash): Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent. -- verify-data (REASONED; metabase-docs, superset, redash-docs): Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive. -- verify-private (REASONED; redash-docs, docker): For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path. +- spf-identity (REASONED; spf, dmarc): SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment. +- spf-all (REASONED; spf): -all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone. +- spf-record (REASONED; spf): Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror. +- spf-lookups (REASONED; spf): Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass. +- dkim-signing (REASONED; dkim, dmarc): DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC. +- dkim-crypto (REASONED; crypto): Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example. +- dkim-rotation (REASONED; dkim): Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership. +- dmarc-alignment (REASONED; dmarc): DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching. +- dmarc-policy (REASONED; dmarc): p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery. +- dmarc-subdomains (REASONED; dmarc): sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies. +- dmarc-discovery (REASONED; dmarc): RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method. +- dmarc-testing (REASONED; dmarc): t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag. +- dmarc-rollout (REASONED; dmarc, aggregate): Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected. +- dmarc-pct (REASONED; dmarc): RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control. +- aggregate (REASONED; aggregate): Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked. +- failure-reports (REASONED; failure): ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations. +- starttls (REASONED; submission): 587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS. +- implicit-tls (REASONED; submission): 465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward. +- tls-validation (REASONED; submission, submission-update): Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997. +- port-25 (REASONED; roles): 25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication. +- relay-policy (REASONED; relay): Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths. +- submission-auth (REASONED; roles, submission): After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds. +- sts-publication (REASONED; sts): MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800. +- sts-enforcement (REASONED; sts): Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail. +- sts-refresh (REASONED; sts): Update the MTA-STS TXT id whenever the policy changes. +- tls-reports (REASONED; rpt): TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement. +- bimi (REASONED; bimi): BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last. +- verify-dns (REASONED; spf, dkim, dmarc, sts, rpt, bimi, bind): Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available. +- verify-tls (REASONED; submission, submission-update, openssl): 587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available. +- verify-alignment (REASONED; results, dmarc): Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling. +- verify-relay (REASONED; relay, roles): In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved. + +````json +{ + "schema": 1, + "checked": "2026-10-05", + "documentation_checked": "2026-09", + "body_sha256": "61afa40371c3be4eb020d413469c8c6970e9a28a27ea6defa3c0ba95cf00df88", + "components": { + "spf": { + "name": "SPF", + "basis": "RFC 7208", + "sources": { + "sea6ac206b034": "https://www.rfc-editor.org/rfc/rfc7208.html" + } + }, + "dkim": { + "name": "DKIM", + "basis": "RFC 6376", + "sources": { + "s13ee1bc66dfc": "https://www.rfc-editor.org/rfc/rfc6376.html" + } + }, + "crypto": { + "name": "DKIM cryptography", + "basis": "RFC 8301", + "sources": { + "sb675e6772778": "https://www.rfc-editor.org/rfc/rfc8301.html" + } + }, + "dmarc": { + "name": "DMARC", + "basis": "RFC 9989", + "sources": { + "s5da292ff1b92": "https://www.rfc-editor.org/rfc/rfc9989.html" + } + }, + "aggregate": { + "name": "DMARC aggregate reporting", + "basis": "RFC 9990", + "sources": { + "s5e0e418f74dd": "https://www.rfc-editor.org/rfc/rfc9990.html" + } + }, + "failure": { + "name": "DMARC failure reporting", + "basis": "RFC 9991", + "sources": { + "s136bbcd8e31a": "https://www.rfc-editor.org/rfc/rfc9991.html" + } + }, + "submission": { + "name": "Submission TLS", + "basis": "RFC 8314", + "sources": { + "sd78efc15555c": "https://www.rfc-editor.org/rfc/rfc8314.html" + } + }, + "roles": { + "name": "SMTP submission roles", + "basis": "RFC 6409", + "sources": { + "s3f9972faf8ff": "https://www.rfc-editor.org/rfc/rfc6409.html" + } + }, + "relay": { + "name": "SMTP relay controls", + "basis": "RFC 2505", + "sources": { + "s1b9349dd99bc": "https://www.rfc-editor.org/rfc/rfc2505.html" + } + }, + "sts": { + "name": "MTA-STS", + "basis": "RFC 8461", + "sources": { + "s39819f3ea93a": "https://www.rfc-editor.org/rfc/rfc8461.html" + } + }, + "rpt": { + "name": "TLS-RPT", + "basis": "RFC 8460", + "sources": { + "se80da0f5e0c8": "https://www.rfc-editor.org/rfc/rfc8460.html" + } + }, + "results": { + "name": "Authentication-Results", + "basis": "RFC 8601", + "sources": { + "s46f8392d93fd": "https://www.rfc-editor.org/rfc/rfc8601.html" + } + }, + "bimi": { + "name": "BIMI documentation", + "basis": "unknown", + "sources": { + "sd18b1e0134bd": "https://bimigroup.org/how-and-why-to-implement-bimi-selectors/" + } + }, + "submission-update": { + "name": "Submission TLS update", + "basis": "RFC 8997", + "sources": { + "sb5ca35fe0d1b": "https://www.rfc-editor.org/rfc/rfc8997.html#section-3" + } + }, + "bind": { + "name": "BIND dig manual", + "basis": "9.18.39", + "sources": { + "s54cff06bb882": "https://bind9.readthedocs.io/en/v9.18.39/manpages.html" + } + }, + "openssl": { + "name": "OpenSSL command documentation", + "basis": "3.0", + "sources": { + "s00aaf106164f": "https://docs.openssl.org/3.0/man1/openssl-s_client/", + "s2a2127a2c79b": "https://docs.openssl.org/3.0/man3/SSL_CONF_cmd/" + } + } + }, + "claims": { + "spf-identity": {"text": "SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment.", "components": ["spf", "dmarc"], "sources": ["spf:sea6ac206b034", "dmarc:s5da292ff1b92"], "status": "REASONED"}, + "spf-all": {"text": "-all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone.", "components": ["spf"], "sources": ["spf:sea6ac206b034"], "status": "REASONED"}, + "spf-record": {"text": "Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror.", "components": ["spf"], "sources": ["spf:sea6ac206b034"], "status": "REASONED"}, + "spf-lookups": {"text": "Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass.", "components": ["spf"], "sources": ["spf:sea6ac206b034"], "status": "REASONED"}, + "dkim-signing": {"text": "DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC.", "components": ["dkim", "dmarc"], "sources": ["dkim:s13ee1bc66dfc", "dmarc:s5da292ff1b92"], "status": "REASONED"}, + "dkim-crypto": {"text": "Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example.", "components": ["crypto"], "sources": ["crypto:sb675e6772778"], "status": "REASONED"}, + "dkim-rotation": {"text": "Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership.", "components": ["dkim"], "sources": ["dkim:s13ee1bc66dfc"], "status": "REASONED"}, + "dmarc-alignment": {"text": "DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching.", "components": ["dmarc"], "sources": ["dmarc:s5da292ff1b92"], "status": "REASONED"}, + "dmarc-policy": {"text": "p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery.", "components": ["dmarc"], "sources": ["dmarc:s5da292ff1b92"], "status": "REASONED"}, + "dmarc-subdomains": {"text": "sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies.", "components": ["dmarc"], "sources": ["dmarc:s5da292ff1b92"], "status": "REASONED"}, + "dmarc-discovery": {"text": "RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method.", "components": ["dmarc"], "sources": ["dmarc:s5da292ff1b92"], "status": "REASONED"}, + "dmarc-testing": {"text": "t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag.", "components": ["dmarc"], "sources": ["dmarc:s5da292ff1b92"], "status": "REASONED"}, + "dmarc-rollout": {"text": "Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected.", "components": ["dmarc", "aggregate"], "sources": ["dmarc:s5da292ff1b92", "aggregate:s5e0e418f74dd"], "status": "REASONED"}, + "dmarc-pct": {"text": "RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control.", "components": ["dmarc"], "sources": ["dmarc:s5da292ff1b92"], "status": "REASONED"}, + "aggregate": {"text": "Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked.", "components": ["aggregate"], "sources": ["aggregate:s5e0e418f74dd"], "status": "REASONED"}, + "failure-reports": {"text": "ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations.", "components": ["failure"], "sources": ["failure:s136bbcd8e31a"], "status": "REASONED"}, + "starttls": {"text": "587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS.", "components": ["submission"], "sources": ["submission:sd78efc15555c"], "status": "REASONED"}, + "implicit-tls": {"text": "465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward.", "components": ["submission"], "sources": ["submission:sd78efc15555c"], "status": "REASONED"}, + "tls-validation": {"text": "Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997.", "components": ["submission", "submission-update"], "sources": ["submission:sd78efc15555c", "submission-update:sb5ca35fe0d1b"], "status": "REASONED"}, + "port-25": {"text": "25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication.", "components": ["roles"], "sources": ["roles:s3f9972faf8ff"], "status": "REASONED"}, + "relay-policy": {"text": "Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths.", "components": ["relay"], "sources": ["relay:s1b9349dd99bc"], "status": "REASONED"}, + "submission-auth": {"text": "After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds.", "components": ["roles", "submission"], "sources": ["roles:s3f9972faf8ff", "submission:sd78efc15555c"], "status": "REASONED"}, + "sts-publication": {"text": "MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800.", "components": ["sts"], "sources": ["sts:s39819f3ea93a"], "status": "REASONED"}, + "sts-enforcement": {"text": "Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail.", "components": ["sts"], "sources": ["sts:s39819f3ea93a"], "status": "REASONED"}, + "sts-refresh": {"text": "Update the MTA-STS TXT id whenever the policy changes.", "components": ["sts"], "sources": ["sts:s39819f3ea93a"], "status": "REASONED"}, + "tls-reports": {"text": "TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement.", "components": ["rpt"], "sources": ["rpt:se80da0f5e0c8"], "status": "REASONED"}, + "bimi": {"text": "BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last.", "components": ["bimi"], "sources": ["bimi:sd18b1e0134bd"], "status": "REASONED"}, + "verify-dns": {"text": "Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available.", "components": ["spf", "dkim", "dmarc", "sts", "rpt", "bimi", "bind"], "sources": ["spf:sea6ac206b034", "dkim:s13ee1bc66dfc", "dmarc:s5da292ff1b92", "sts:s39819f3ea93a", "rpt:se80da0f5e0c8", "bimi:sd18b1e0134bd", "bind:s54cff06bb882"], "status": "REASONED", "verify": [1]}, + "verify-tls": {"text": "587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available.", "components": ["submission", "submission-update", "openssl"], "sources": ["submission:sd78efc15555c", "submission-update:sb5ca35fe0d1b", "openssl:s00aaf106164f", "openssl:s2a2127a2c79b"], "status": "REASONED", "verify": [1]}, + "verify-alignment": {"text": "Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling.", "components": ["results", "dmarc"], "sources": ["results:s46f8392d93fd", "dmarc:s5da292ff1b92"], "status": "REASONED"}, + "verify-relay": {"text": "In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved.", "components": ["relay", "roles"], "sources": ["relay:s1b9349dd99bc", "roles:s3f9972faf8ff"], "status": "REASONED"} + } +} +```` +====================================================================== +==> bi-dashboards.md +====================================================================== + +# BI dashboards: Metabase, Superset, Redash + +Written against Metabase v0.63.18, Redash v26.9.0, environ 1.2.0, Ring Jetty adapter 1.15.3, and Jetty 12.1.13; 1 further source is pinned to a commit and 4 carry no recorded version. Metadata reviewed 2026-09-26; documentation checked September 2026. The guide records 25 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. + These tools hold live connections to your production databases and cache query results in their own storage. An exposed instance, or one still running default or example credentials, leaks both the dashboards themselves and the databases behind them. One rule dominates everything tool-specific @@ -53523,88 +56819,209 @@ connection string or the tool's own login is not enough. - Metabase database users, roles, and privileges (a dedicated least-privilege read-only account): https://www.metabase.com/docs/latest/databases/users-roles-privileges - Docker port publishing (a published container port is served through NAT/forwarding rules, so the host may show no matching listener): https://docs.docker.com/engine/network/port-publishing/ -====================================================================== -==> low-code-builders.md -====================================================================== - -# Low-code internal-tool builders: NocoDB, Baserow, Appsmith, Budibase, and Windmill +## 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/bi-dashboards.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- nocodb: NocoDB (2026.09.0) -- docker: Docker docs (unknown) -- docker-new: Docker CLI (v27.2.0) -- docker-old: Docker CLI (v27.1.2) -- node: Node.js (v22.22.1) -- baserow: Baserow (2.3.4) -- appsmith: Appsmith (v2.4.1) -- budibase: Budibase (v3.46.0) -- couchdb: CouchDB (3.5.2) -- litellm: LiteLLM authentication source (6ef7b86748118ceecd95271727c2fa167ce55fec) -- windmill: Windmill (v1.817.0) +- metabase: Metabase (v0.63.18) +- redash: Redash (v26.9.0) +- metabase-docs: Metabase docs (unknown) +- superset: Superset docs (unknown) +- superset-compose: Superset Compose (3d01094d231712e3470eb625be3f9925aeaeadb5) +- redash-docs: Redash docs (unknown) +- environ: environ (1.2.0) +- ring: Ring Jetty adapter (1.15.3) +- jetty: Jetty (12.1.13) +- docker: Docker publishing (unknown) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- private (REASONED; nocodb, baserow, appsmith, budibase, windmill): Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials. -- nocodb-bind (REASONED; nocodb, node): PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall. -- nocodb-first (REASONED; nocodb): First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD. -- nocodb-signup (REASONED; nocodb): invite_only_signup defaults false; enable to stop later self-signups. -- nocodb-encryption (REASONED; nocodb): Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later. -- nocodb-jwt (REASONED; nocodb): Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010. -- nocodb-cors (REASONED; nocodb): Entry file allows every CORS origin. -- nocodb-proxy (REASONED; nocodb): Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access. -- nocodb-packaging (REASONED; nocodb): README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice). -- baserow-bind (REASONED; baserow): Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW. -- baserow-first (REASONED; baserow): First signup becomes staff; claim before exposure. -- baserow-signup (REASONED; baserow): allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled. -- baserow-secrets (REASONED; baserow): Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own. -- baserow-mfa (REASONED; baserow): Core registers a TOTP provider. -- appsmith-bind (REASONED; appsmith): Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container. -- appsmith-publish (REASONED; appsmith): Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443. -- appsmith-first (REASONED; appsmith): First signup claims super-user while no users exist; claim before exposure. -- appsmith-signup (REASONED; appsmith): APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED. -- appsmith-encryption (REASONED; appsmith): Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start. -- appsmith-rotation (REASONED; appsmith): Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested. -- budibase-publish (REASONED; budibase): Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000. -- budibase-couchdb (REASONED; budibase): Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase. -- budibase-secrets (REASONED; budibase): Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established. -- budibase-sample-check (REASONED; budibase): This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran. -- budibase-init (REASONED; budibase): POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin. -- budibase-seed (REASONED; budibase): Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true. -- windmill-code (REASONED; windmill): Workers execute user scripts; a login enables code execution on worker hosts. -- windmill-bind (REASONED; windmill): Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed. -- windmill-compose (REASONED; windmill): Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred. -- windmill-sandbox (REASONED; windmill): DISABLE_NSJAIL defaults true; sandboxing is not enabled by default. -- windmill-privileged (REASONED; windmill): Shipped worker uses privileged=true. -- windmill-smtp (REASONED; windmill): Port 25 serves email triggers; do not publish unless used. -- windmill-debugger (REASONED; windmill): Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments. -- verify-nocodb-signup (REASONED; nocodb): Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login. -- verify-baserow-signup (REASONED; baserow): Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login. -- verify-appsmith-signup (REASONED; appsmith): Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts. -- verify-external (REASONED; baserow, budibase, windmill): Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation. -- windmill-default-login (DEMONSTRATED; windmill): Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200. -- windmill-loopback (DEMONSTRATED; windmill): SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port. -- windmill-sandbox-run (DEMONSTRATED; windmill): Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation. -- verify-inventory (REASONED; docker, baserow, appsmith, budibase, windmill): Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation. -- verify-port-display (REASONED; docker, docker-new, docker-old): IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered. -- verify-sample-tokens (DEMONSTRATED; nocodb, appsmith, budibase, windmill): Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope. -- verify-nocodb-key (REASONED; nocodb, docker): docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified. -- nocodb-key-limits (REASONED; nocodb, docker): Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key. -- nocodb-key-logic (DEMONSTRATED; docker): A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated. -- verify-windmill-control (REASONED; windmill): Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed. -- verify-couchdb (REASONED; budibase, couchdb): Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control. -- verify-litellm (REASONED; budibase, litellm): Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched. -- verify-tcp-outcomes (DEMONSTRATED; baserow, budibase, windmill): Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated. -- verify-tcp-inputs (DEMONSTRATED; baserow, budibase, windmill): Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control. +- private (REASONED; redash-docs, metabase-docs): Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed. +- metabase-setup (REASONED; metabase-docs): First account is admin; finish setup before anyone else can reach it. +- metabase-precedence (REASONED; metabase, environ): Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings. +- metabase-bind (REASONED; metabase, ring, jetty): Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy. +- metabase-image (REASONED; metabase, docker): Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start. +- metabase-port (REASONED; metabase): Port defaults 3000 unless the final MB_JETTY_PORT is nonblank. +- metabase-sharing (REASONED; metabase-docs): Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing. +- metabase-embed (REASONED; metabase-docs): Public embed URLs are recoverable; embeds do not strengthen the public-link boundary. +- metabase-sandbox (REASONED; metabase-docs): Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries. +- superset-secret (REASONED; superset): Set a unique complex random SECRET_KEY before exposure; it signs session cookies. +- superset-admin (REASONED; superset, superset-compose): Replace the first-run admin password before exposure. +- superset-public (REASONED; superset): Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default. +- superset-copy (REASONED; superset): PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards. +- superset-compose (REASONED; superset-compose): Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data. +- redash-setup (REASONED; redash-docs): Create the first-run admin before others reach setup. +- redash-https (REASONED; redash-docs): Enforce HTTPS and the private-access pattern. +- redash-cookie (REASONED; redash-docs): Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it. +- redash-encryption (REASONED; redash-docs): REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control. +- redash-bind (REASONED; redash): Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately. +- verify-credentials (REASONED; metabase-docs, superset-compose, redash-docs): Confirm no default admin/admin or example credentials work. +- verify-db (REASONED; metabase-docs): Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately. +- verify-listeners (REASONED; metabase, superset-compose, redash, docker): Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation. +- verify-ports (REASONED; metabase, superset-compose, redash): Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent. +- verify-data (REASONED; metabase-docs, superset, redash-docs): Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive. +- verify-private (REASONED; redash-docs, docker): For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path. + +````json +{ + "schema": 1, + "checked": "2026-09-26", + "documentation_checked": "2026-09", + "body_sha256": "72fb83f126c17a7bf6712bea8620686191699a388b522b41778ca65b8b8c506f", + "components": { + "metabase": { + "name": "Metabase", + "basis": "v0.63.18", + "sources": { + "s8df4cbf03036": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L54", + "s7dbf92797642": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/config/core.clj#L76-L88", + "s7e71ce4cff1b": "https://github.com/metabase/metabase/blob/v0.63.18/src/metabase/server/instance.clj#L36-L41", + "sed761829c85c": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L79", + "sea59c4ef7f2e": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L164", + "sabe1aa2cf0a9": "https://github.com/metabase/metabase/blob/v0.63.18/deps.edn#L195", + "s18f6f2188fe9": "https://github.com/metabase/metabase/blob/v0.63.18/bin/docker/run_metabase.sh#L2-L5", + "s9b0c73c666ff": "https://github.com/metabase/metabase/blob/v0.63.18/Dockerfile#L66-L69" + } + }, + "redash": { + "name": "Redash", + "basis": "v26.9.0", + "sources": { + "se93d7c47b037": "https://github.com/getredash/redash/blob/v26.9.0/bin/docker-entrypoint#L49-L50", + "s9f1ef7237804": "https://github.com/getredash/redash/blob/v26.9.0/Dockerfile#L133-L134" + } + }, + "metabase-docs": { + "name": "Metabase docs", + "basis": "unknown", + "sources": { + "sefe12b1b495c": "https://www.metabase.com/docs/latest/", + "s5e2251b3184d": "https://www.metabase.com/docs/latest/configuring-metabase/setting-up-metabase", + "s521b3063e9f0": "https://www.metabase.com/docs/latest/embedding/public-links", + "s02b7797b6ffe": "https://www.metabase.com/docs/latest/permissions/data", + "s434abdb1aaef": "https://www.metabase.com/docs/latest/databases/users-roles-privileges" + } + }, + "superset": { + "name": "Superset docs", + "basis": "unknown", + "sources": { + "se97be7353585": "https://superset.apache.org/admin-docs/security/" + } + }, + "superset-compose": { + "name": "Superset Compose", + "basis": "3d01094d231712e3470eb625be3f9925aeaeadb5", + "sources": { + "s0ceca7af7f79": "https://github.com/apache/superset/blob/3d01094d231712e3470eb625be3f9925aeaeadb5/docker-compose.yml" + } + }, + "redash-docs": { + "name": "Redash docs", + "basis": "unknown", + "sources": { + "s069a74340b65": "https://redash.io/help/", + "s4c72252b5447": "https://redash.io/help/open-source/setup/", + "sbaf5b44079a0": "https://redash.io/help/open-source/admin-guide/secrets/" + } + }, + "environ": { + "name": "environ", + "basis": "1.2.0", + "sources": { + "s37ed6de2d80d": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L32-L43", + "s9754f7c47384": "https://github.com/weavejester/environ/blob/1.2.0/environ/src/environ/core.cljc#L64-L79" + } + }, + "ring": { + "name": "Ring Jetty adapter", + "basis": "1.15.3", + "sources": { + "s0455b1b1fb12": "https://github.com/ring-clojure/ring/blob/1.15.3/ring-jetty-adapter/src/ring/adapter/jetty.clj#L202-L207" + } + }, + "jetty": { + "name": "Jetty", + "basis": "12.1.13", + "sources": { + "s8f9e4989175e": "https://github.com/jetty/jetty.project/blob/jetty-12.1.13/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java#L338-L344" + } + }, + "docker": { + "name": "Docker publishing", + "basis": "unknown", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + } + }, + "claims": { + "private": {"text": "Keep BI tools private with fronting MFA and least-privilege read-only DB accounts unless write-back is needed.", "components": ["redash-docs", "metabase-docs"], "sources": ["redash-docs:s4c72252b5447", "metabase-docs:s434abdb1aaef"], "status": "REASONED"}, + "metabase-setup": {"text": "First account is admin; finish setup before anyone else can reach it.", "components": ["metabase-docs"], "sources": ["metabase-docs:s5e2251b3184d"], "status": "REASONED"}, + "metabase-precedence": {"text": "Settings merge .lein-env, .boot-env, environment and JVM properties in order; later empty values mask earlier settings.", "components": ["metabase", "environ"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "environ:s37ed6de2d80d", "environ:s9754f7c47384"], "status": "REASONED"}, + "metabase-bind": {"text": "Final nonblank MB_JETTY_HOST sets the jar host; otherwise Ring passes no host and Jetty binds wildcard. Use loopback behind a local proxy.", "components": ["metabase", "ring", "jetty"], "sources": ["metabase:s7e71ce4cff1b", "metabase:sed761829c85c", "metabase:sea59c4ef7f2e", "metabase:sabe1aa2cf0a9", "ring:s0455b1b1fb12", "jetty:s8f9e4989175e"], "status": "REASONED"}, + "metabase-image": {"text": "Image exports MB_JETTY_HOST=0.0.0.0 when unset/empty; publish 127.0.0.1:3000:3000 before first start.", "components": ["metabase", "docker"], "sources": ["metabase:s18f6f2188fe9", "metabase:s9b0c73c666ff", "docker:s1e53417c513d"], "status": "REASONED"}, + "metabase-port": {"text": "Port defaults 3000 unless the final MB_JETTY_PORT is nonblank.", "components": ["metabase"], "sources": ["metabase:s8df4cbf03036", "metabase:s7dbf92797642", "metabase:s7e71ce4cff1b"], "status": "REASONED"}, + "metabase-sharing": {"text": "Public links/embeds default enabled and expose view-only question/dashboard/document results without login; disable unneeded sharing.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, + "metabase-embed": {"text": "Public embed URLs are recoverable; embeds do not strengthen the public-link boundary.", "components": ["metabase-docs"], "sources": ["metabase-docs:s521b3063e9f0"], "status": "REASONED"}, + "metabase-sandbox": {"text": "Row/column security is Pro/Enterprise; free-tier sharing exposes the rows/columns the question queries.", "components": ["metabase-docs"], "sources": ["metabase-docs:s02b7797b6ffe"], "status": "REASONED"}, + "superset-secret": {"text": "Set a unique complex random SECRET_KEY before exposure; it signs session cookies.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-admin": {"text": "Replace the first-run admin password before exposure.", "components": ["superset", "superset-compose"], "sources": ["superset:se97be7353585", "superset-compose:s0ceca7af7f79"], "status": "REASONED"}, + "superset-public": {"text": "Absent AUTH_ROLE_PUBLIC does not prove denial; inspect effective Public-role grants, which provide no data access by default.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-copy": {"text": "PUBLIC_ROLE_LIKE copies permissions at superset init; broad roles grant access without scoping datasets/dashboards.", "components": ["superset"], "sources": ["superset:se97be7353585"], "status": "REASONED"}, + "superset-compose": {"text": "Example Compose is unsupported for production; use unique passwords/SECRET_KEY, not the example against production data.", "components": ["superset-compose"], "sources": ["superset-compose:s0ceca7af7f79"], "status": "REASONED"}, + "redash-setup": {"text": "Create the first-run admin before others reach setup.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, + "redash-https": {"text": "Enforce HTTPS and the private-access pattern.", "components": ["redash-docs"], "sources": ["redash-docs:s4c72252b5447"], "status": "REASONED"}, + "redash-cookie": {"text": "Use an instance-specific REDASH_COOKIE_SECRET; never commit or reuse it.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, + "redash-encryption": {"text": "REDASH_SECRET_KEY encrypts data-source credentials; use a strong unique value outside version control.", "components": ["redash-docs"], "sources": ["redash-docs:sbaf5b44079a0"], "status": "REASONED"}, + "redash-bind": {"text": "Default image server uses REDASH_GUNICORN_BIND or [::]:5000 when unset/empty; dual-stack may accept IPv4. Bind/publish privately.", "components": ["redash"], "sources": ["redash:se93d7c47b037", "redash:s9f1ef7237804"], "status": "REASONED"}, + "verify-credentials": {"text": "Confirm no default admin/admin or example credentials work.", "components": ["metabase-docs", "superset-compose", "redash-docs"], "sources": ["metabase-docs:s5e2251b3184d", "superset-compose:s0ceca7af7f79", "redash-docs:s4c72252b5447"], "status": "REASONED"}, + "verify-db": {"text": "Inspect effective/inherited DB grants; confirm reads and denied writes on a disposable object; scope needed write-back separately.", "components": ["metabase-docs"], "sources": ["metabase-docs:s434abdb1aaef"], "status": "REASONED"}, + "verify-listeners": {"text": "Inventory application/proxy sockets and Docker NAT publications; probe direct origins over IPv4/IPv6/LAN/VPC. Absent ss entries do not prove isolation.", "components": ["metabase", "superset-compose", "redash", "docker"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]}, + "verify-ports": {"text": "Verify lists Metabase/Superset/Redash defaults 3000/8088/5000, qualified as install-dependent.", "components": ["metabase", "superset-compose", "redash"], "sources": ["metabase:s8df4cbf03036", "superset-compose:s0ceca7af7f79", "redash:se93d7c47b037"], "status": "REASONED", "verify": [1]}, + "verify-data": {"text": "Authorized data is the control; anonymous data fails. Denial/verified login challenge passes; SPA shells, 404/errors and arbitrary redirects are inconclusive.", "components": ["metabase-docs", "superset", "redash-docs"], "sources": ["metabase-docs:s521b3063e9f0", "superset:se97be7353585", "redash-docs:s4c72252b5447"], "status": "REASONED", "verify": [1]}, + "verify-private": {"text": "For tunnel-only access, confirm public isolation separately and test authorization on the permitted private path.", "components": ["redash-docs", "docker"], "sources": ["redash-docs:s4c72252b5447", "docker:s1e53417c513d"], "status": "REASONED", "verify": [1]} + } +} +```` +====================================================================== +==> low-code-builders.md +====================================================================== + +# Low-code internal-tool builders: NocoDB, Baserow, Appsmith, Budibase, and Windmill + +Written against NocoDB 2026.09.0, Baserow 2.3.4, Appsmith v2.4.1, Budibase v3.46.0, Windmill v1.817.0, Docker CLI v27.2.0 and v27.1.2, Node.js v22.22.1, and CouchDB 3.5.2; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 51 claims: 7 demonstrated by a check that ran and 44 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 tools sit on top of your databases and APIs and let people build internal apps, forms and automations quickly. That is also the exposure. Each one stores credentials for the systems it connects to, and Windmill runs arbitrary code by design. Three first-run patterns recur. On a fresh instance, the @@ -54064,73 +57481,315 @@ mean to publish is the finding. - Windmill v1.817.0 Compose file: https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml - Windmill v1.817.0 seeded-user migrations: https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql and https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql -====================================================================== -==> server-admin-panels.md -====================================================================== - -# Server administration panels: Cockpit, Webmin, and Proxmox VE +## 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/low-code-builders.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-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. +DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded. Components (recorded basis): -- cockpit: Cockpit (368) -- systemd: systemd (v262) -- webmin: Webmin (2.670) -- webmin-docs: Webmin docs (8ceae26c5a074053905cbcc6c0053be573633f3a) -- usermin: Usermin (2.570) -- pve-docs: Proxmox VE 9.2 docs (9.2.12) -- pve-manager: pve-manager (9.2.20) -- pve-http: Proxmox HTTP server (5119ff9bec08c69584c0c98bea3edd0098179e5f) -- pve-common: libpve-common-perl 9.1.21 (5054082fe492429fc37574985c2ca812af9a3125) +- nocodb: NocoDB (2026.09.0) +- baserow: Baserow (2.3.4) +- appsmith: Appsmith (v2.4.1) +- budibase: Budibase (v3.46.0) +- windmill: Windmill (v1.817.0) +- docker: Docker docs (unknown) +- docker-new: Docker CLI (v27.2.0) +- docker-old: Docker CLI (v27.1.2) +- node: Node.js (v22.22.1) +- couchdb: CouchDB (3.5.2) +- litellm: LiteLLM authentication source (6ef7b86748118ceecd95271727c2fa167ce55fec) -Claims (full source mapping in the front matter): +Claims (full source mapping in the JSON block below): -- private (REASONED; cockpit, webmin-docs, pve-docs): Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor. -- cockpit-bind (REASONED; cockpit, systemd): Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses. -- cockpit-dropin (REASONED; cockpit): Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart. -- cockpit-tls (REASONED; cockpit): One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation. -- cockpit-pam (REASONED; cockpit): Local accounts use Cockpit PAM with normal SSH-equivalent privileges. -- cockpit-root (REASONED; cockpit): First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable. -- cockpit-escalation (REASONED; cockpit): Users allowed sudo/polkit escalation get an immediately elevated session and terminal. -- cockpit-mfa (REASONED; cockpit): No built-in second factor; use PAM, Kerberos or client certificates. -- cockpit-rate (REASONED; cockpit): MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout. -- cockpit-loginto (REASONED; cockpit): Set LoginTo=false to prevent unauthenticated scanning of reachable private networks. -- webmin-bind (REASONED; webmin, usermin): Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses. -- webmin-discovery (REASONED; webmin, usermin): Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused. -- webmin-access (REASONED; webmin, webmin-docs): Set management bind and allow networks, then restart; empty allow admits all except denied clients. -- webmin-tls (REASONED; webmin): Deb/RPM packages enable ssl=1 with a self-signed certificate. -- webmin-root (REASONED; webmin): Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root. -- webmin-sudo (REASONED; webmin): Installed sudo=1 lets sudo-permitted Unix users log in effectively as root. -- webmin-mfa (REASONED; webmin): TOTP/Authy exists but is not configured at install; enable and enrol every user. -- webmin-advisories (REASONED; webmin-docs): Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release. -- webmin-rate (REASONED; webmin): Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1. -- pve-bind (REASONED; pve-docs, pve-manager, pve-http, pve-common): pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified. -- pve-cluster (REASONED; pve-docs): The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules. -- pve-acl (REASONED; pve-docs, pve-manager, pve-http): Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone. -- pve-firewall (REASONED; pve-docs): Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses. -- pve-root (REASONED; pve-docs): root@pam can always log in as unconfined admin; installer sets the web-interface root password. -- pve-shell (REASONED; pve-manager): Node shell opens /bin/login -f root without another password. -- pve-mfa (REASONED; pve-docs): Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor. -- pve-lockout (REASONED; pve-docs): Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts. -- pve-password-delay (REASONED; pve-http): Unauthorized API responses delay three seconds; this is not password lockout. -- pve-certificates (REASONED; pve-docs): Each cluster creates its own self-signed CA. -- pve-tokens (REASONED; pve-docs): API tokens default to separated privileges and cannot access VM/node consoles. -- verify-cockpit (REASONED; cockpit): Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults. -- verify-pve (REASONED; pve-docs, pve-manager, pve-http, pve-common): Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings. -- verify-external (REASONED; cockpit, webmin, usermin, pve-docs): Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP. -- webmin-loopback (DEMONSTRATED; webmin): Loopback bind with no listen line produced one TCP socket and no UDP. -- webmin-rate-run (DEMONSTRATED; webmin): Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403. -- verify-listeners (REASONED; cockpit, webmin, usermin, pve-docs, pve-manager, pve-http, pve-common): Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host. -- verify-webmin-parser (DEMONSTRATED; webmin): Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof. -- verify-tcp-loopback (DEMONSTRATED; cockpit, webmin, usermin, pve-docs): Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard. +- private (REASONED; nocodb, baserow, appsmith, budibase, windmill): Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials. +- nocodb-bind (REASONED; nocodb, node): PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall. +- nocodb-first (REASONED; nocodb): First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD. +- nocodb-signup (REASONED; nocodb): invite_only_signup defaults false; enable to stop later self-signups. +- nocodb-encryption (REASONED; nocodb): Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later. +- nocodb-jwt (REASONED; nocodb): Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010. +- nocodb-cors (REASONED; nocodb): Entry file allows every CORS origin. +- nocodb-proxy (REASONED; nocodb): Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access. +- nocodb-packaging (REASONED; nocodb): README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice). +- baserow-bind (REASONED; baserow): Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW. +- baserow-first (REASONED; baserow): First signup becomes staff; claim before exposure. +- baserow-signup (REASONED; baserow): allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled. +- baserow-secrets (REASONED; baserow): Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own. +- baserow-mfa (REASONED; baserow): Core registers a TOTP provider. +- appsmith-bind (REASONED; appsmith): Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container. +- appsmith-publish (REASONED; appsmith): Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443. +- appsmith-first (REASONED; appsmith): First signup claims super-user while no users exist; claim before exposure. +- appsmith-signup (REASONED; appsmith): APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED. +- appsmith-encryption (REASONED; appsmith): Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start. +- appsmith-rotation (REASONED; appsmith): Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested. +- budibase-publish (REASONED; budibase): Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000. +- budibase-couchdb (REASONED; budibase): Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase. +- budibase-secrets (REASONED; budibase): Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established. +- budibase-sample-check (REASONED; budibase): This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran. +- budibase-init (REASONED; budibase): POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin. +- budibase-seed (REASONED; budibase): Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true. +- windmill-code (REASONED; windmill): Workers execute user scripts; a login enables code execution on worker hosts. +- windmill-bind (REASONED; windmill): Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed. +- windmill-compose (REASONED; windmill): Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred. +- windmill-sandbox (REASONED; windmill): DISABLE_NSJAIL defaults true; sandboxing is not enabled by default. +- windmill-privileged (REASONED; windmill): Shipped worker uses privileged=true. +- windmill-smtp (REASONED; windmill): Port 25 serves email triggers; do not publish unless used. +- windmill-debugger (REASONED; windmill): Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments. +- verify-nocodb-signup (REASONED; nocodb): Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login. +- verify-baserow-signup (REASONED; baserow): Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login. +- verify-appsmith-signup (REASONED; appsmith): Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts. +- verify-external (REASONED; baserow, budibase, windmill): Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation. +- windmill-default-login (DEMONSTRATED; windmill): Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200. +- windmill-loopback (DEMONSTRATED; windmill): SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port. +- windmill-sandbox-run (DEMONSTRATED; windmill): Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation. +- verify-inventory (REASONED; docker, baserow, appsmith, budibase, windmill): Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation. +- verify-port-display (REASONED; docker, docker-new, docker-old): IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered. +- verify-sample-tokens (DEMONSTRATED; nocodb, appsmith, budibase, windmill): Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope. +- verify-nocodb-key (REASONED; nocodb, docker): docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified. +- nocodb-key-limits (REASONED; nocodb, docker): Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key. +- nocodb-key-logic (DEMONSTRATED; docker): A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated. +- verify-windmill-control (REASONED; windmill): Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed. +- verify-couchdb (REASONED; budibase, couchdb): Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control. +- verify-litellm (REASONED; budibase, litellm): Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched. +- verify-tcp-outcomes (DEMONSTRATED; baserow, budibase, windmill): Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated. +- verify-tcp-inputs (DEMONSTRATED; baserow, budibase, windmill): Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "43cf22968b497a23408d386c38d01f763e391f115cf9a92f4fb443ce28cea9a8", + "components": { + "nocodb": { + "name": "NocoDB", + "basis": "2026.09.0", + "sources": { + "sa19c6557b712": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/Noco.ts", + "s635b35ce4689": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/trustProxy.ts", + "sd74ca4df55b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/run/dockerEntry.ts", + "s5ea55dc5f0d9": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/services/users/users.service.ts", + "sd0e9f451673f": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/interface/AppSettings.ts", + "s8a1de0bc97b0": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initAdminFromEnv.ts", + "sca40040b40cc": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/utils/encryptDecrypt.ts", + "saf056bb4ff37": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/src/helpers/initDataSourceEncryption.ts", + "sbfbf630008e1": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start.sh", + "se5da9707c684": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/docker/start-litestream.sh", + "s536899ecd4a5": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md", + "sca0c9c0ecf24": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L56-L75", + "sb4d5b659f7b8": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L77-L90", + "s2769b9054778": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L82", + "s2b16e2c93606": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L90", + "s118384fa1d9f": "https://github.com/nocodb/nocodb/blob/2026.09.0/README.md#L95", + "s9bee4adc612e": "https://api.github.com/repos/nocodb/nocodb/releases/tags/2026.09.0", + "s3f296c702bc5": "https://github.com/nocodb/nocodb/blob/2026.09.0/packages/nocodb/package.json#L3", + "se81c30b3f43b": "https://github.com/nocodb/nocodb/blob/2026.09.0/docker-compose/examples/external-postgres-and-redis/docker.env" + } + }, + "baserow": { + "name": "Baserow", + "basis": "2.3.4", + "sources": { + "s1b31314982f4": "https://github.com/baserow/baserow/blob/2.3.4/docker-compose.yml", + "s3f61036aca03": "https://github.com/baserow/baserow/blob/2.3.4/docs/installation/install-with-docker.md", + "s34b87f60dcc6": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/user/handler.py", + "s4588448964a2": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/models.py", + "sd858f9ee45e0": "https://github.com/baserow/baserow/blob/2.3.4/backend/src/baserow/core/apps.py", + "s1a472d828699": "https://github.com/baserow/baserow/blob/2.3.4/.env.example" + } + }, + "appsmith": { + "name": "Appsmith", + "basis": "v2.4.1", + "sources": { + "s2650629933a1": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/resources/application-ce.properties", + "sd1c55be91e30": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/UserSignupCEImpl.java", + "se3cd894dc579": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java", + "sa68daf87cde7": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/fs/opt/appsmith/entrypoint.sh", + "s9fdcff5a28a4": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/docker/docker-compose.yml", + "sfa551c344e8b": "https://github.com/appsmithorg/appsmith/blob/v2.4.1/deploy/aws_ami/docker-compose.yml" + } + }, + "budibase": { + "name": "Budibase", + "basis": "v3.46.0", + "sources": { + "s735ead81ce9c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env", + "s74d858053d12": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml", + "s7fbf706a4c8f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf", + "s45379b35a980": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts", + "sc0a3ac85466f": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts", + "s4902d93986c7": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts", + "s5209f2549c9b": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/.env#L4-L16", + "s9afbeb2063b5": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L111-L120", + "s32775c45ba51": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L129-L144", + "s4d349b4d4ac3": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L86", + "s2a033b3a4117": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/proxy/nginx.prod.conf#L236-L239", + "s2db467c4f1cf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L40-L43", + "sbd761572fef0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/index.ts#L133-L167", + "sb8f5a18ab4ee": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L13-L28", + "s7883460cabbf": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/routes/global/users.ts#L75-L79", + "se18a9c7182fb": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/worker/src/api/controllers/global/users.ts#L229-L274", + "s816a78b83956": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/server/src/startup/index.ts#L226-L266", + "sedd5e0682bf0": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/src/environment.ts#L267-L268", + "s35ae42bd7a1c": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/docker-compose.yaml#L27-L28", + "scdcd8b6e7071": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L89-L91", + "sc74f36aa8340": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/Dockerfile#L151-L154", + "sf6e2a660380f": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/VERSION#L1", + "sfdc76e71aff6": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/runner.sh#L165-L166", + "s851ad716e105": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/couchdb/docker-entrypoint.sh#L70-L75", + "se993218560de": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/hosting.properties#L4-L18", + "s6179ec0d9abd": "https://github.com/Budibase/budibase/blob/v3.46.0/hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot#L11-L15", + "s9cc27222e133": "https://github.com/Budibase/budibase/blob/v3.46.0/packages/backend-core/tests/jestEnv.ts#L9-L10", + "sbe7e0469e7a8": "https://github.com/Budibase/budibase/blob/v3.46.0/scripts/dev/manage.js#L11-L12" + } + }, + "windmill": { + "name": "Windmill", + "basis": "v1.817.0", + "sources": { + "s91439e21d4ac": "https://github.com/windmill-labs/windmill/blob/v1.817.0/README.md", + "s8e4bbba271f2": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/src/main.rs", + "sd775b7c95872": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/windmill-worker/src/worker.rs", + "sfcc72cf06c9e": "https://github.com/windmill-labs/windmill/blob/v1.817.0/docker-compose.yml", + "s345ca635c837": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220123221903_first.up.sql", + "s03c33b490c08": "https://github.com/windmill-labs/windmill/blob/v1.817.0/backend/migrations/20220816185849_remove_non_admin_users.up.sql" + } + }, + "docker": { + "name": "Docker docs", + "basis": "unknown", + "sources": { + "sd34e76a09f6c": "https://docs.docker.com/reference/cli/docker/container/exec/", + "s2fecb6db5480": "https://docs.docker.com/engine/network/firewall-iptables/" + } + }, + "docker-new": { + "name": "Docker CLI", + "basis": "v27.2.0", + "sources": { + "sca9a0e1e4b6a": "https://github.com/docker/cli/blob/v27.2.0/cli/command/formatter/container.go" + } + }, + "docker-old": { + "name": "Docker CLI", + "basis": "v27.1.2", + "sources": { + "sa202ad11d4ed": "https://github.com/docker/cli/blob/v27.1.2/cli/command/formatter/container.go" + } + }, + "node": { + "name": "Node.js", + "basis": "v22.22.1", + "sources": { + "sfb3e306c102d": "https://github.com/nodejs/node/blob/v22.22.1/doc/api/net.md" + } + }, + "couchdb": { + "name": "CouchDB", + "basis": "3.5.2", + "sources": { + "s34292991c4ec": "https://github.com/apache/couchdb/blob/3.5.2/src/docs/src/api/server/authn.rst" + } + }, + "litellm": { + "name": "LiteLLM authentication source", + "basis": "6ef7b86748118ceecd95271727c2fa167ce55fec", + "sources": { + "sbe6fa0b81017": "https://raw.githubusercontent.com/BerriAI/litellm/6ef7b86748118ceecd95271727c2fa167ce55fec/litellm/proxy/auth/user_api_key_auth.py" + } + } + }, + "claims": { + "private": {"text": "Keep builders private with fronting auth, claim admin early and protect databases storing connected-system credentials.", "components": ["nocodb", "baserow", "appsmith", "budibase", "windmill"], "sources": ["nocodb:sd74ca4df55b0", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "nocodb-bind": {"text": "PORT defaults 8080; listen omits host, so Node binds unspecified IPv6/IPv4. No application bind setting; restrict publication/firewall.", "components": ["nocodb", "node"], "sources": ["nocodb:sa19c6557b712", "nocodb:sd74ca4df55b0", "node:sfb3e306c102d"], "status": "REASONED"}, + "nocodb-first": {"text": "First signup becomes super-admin; claim first or seed NC_ADMIN_EMAIL/NC_ADMIN_PASSWORD.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:s8a1de0bc97b0"], "status": "REASONED"}, + "nocodb-signup": {"text": "invite_only_signup defaults false; enable to stop later self-signups.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, + "nocodb-encryption": {"text": "Unset NC_CONNECTION_ENCRYPT_KEY stores external DB credentials unchanged; set before adding sources, or startup encrypts existing sources when added later.", "components": ["nocodb"], "sources": ["nocodb:sca40040b40cc", "nocodb:saf056bb4ff37"], "status": "REASONED"}, + "nocodb-jwt": {"text": "Unset NC_AUTH_JWT_SECRET generates/stores a UUID as nc_auth_jwt_secret; replace README sample 569a1821-0a93-45e8-87ab-eb857f20a010.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s536899ecd4a5"], "status": "REASONED"}, + "nocodb-cors": {"text": "Entry file allows every CORS origin.", "components": ["nocodb"], "sources": ["nocodb:sd74ca4df55b0"], "status": "REASONED"}, + "nocodb-proxy": {"text": "Startup resets trust proxy from NC_TRUST_PROXY, default no trust; restrict hops/subnets, never true with direct client access.", "components": ["nocodb"], "sources": ["nocodb:sa19c6557b712", "nocodb:s635b35ce4689", "nocodb:sd74ca4df55b0"], "status": "REASONED"}, + "nocodb-packaging": {"text": "README marks binaries for quick local testing and documents Docker image and Auto-upstall paths; the October 2026 release API record lists no assets; the 2026.09.0 tag carries package version 0.301.3. Use the image or vendor install script for production (guide advice). The README command fetches the script over plain HTTP and executes it with Bash; its source link points to develop. Fetch over HTTPS from a pinned, reviewed source and read before running (guide advice).", "components": ["nocodb"], "sources": ["nocodb:s536899ecd4a5", "nocodb:sca0c9c0ecf24", "nocodb:sb4d5b659f7b8", "nocodb:s118384fa1d9f", "nocodb:s9bee4adc612e", "nocodb:s3f296c702bc5", "nocodb:s2769b9054778", "nocodb:s2b16e2c93606"], "status": "REASONED"}, + "baserow-bind": {"text": "Caddy publishes 80/443 on HOST_PUBLISH_IP default 0.0.0.0; set it or publish loopback. Docker wildcard publication bypasses UFW.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s3f61036aca03"], "status": "REASONED"}, + "baserow-first": {"text": "First signup becomes staff; claim before exposure.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6"], "status": "REASONED"}, + "baserow-signup": {"text": "allow_new_signups defaults True; disable in admin settings, yielding Sign up is disabled.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, + "baserow-secrets": {"text": "Compose requires SECRET_KEY/DATABASE_PASSWORD/REDIS_PASSWORD; .env.example leaves them empty. Generate your own.", "components": ["baserow"], "sources": ["baserow:s1b31314982f4", "baserow:s1a472d828699"], "status": "REASONED"}, + "baserow-mfa": {"text": "Core registers a TOTP provider.", "components": ["baserow"], "sources": ["baserow:sd858f9ee45e0"], "status": "REASONED"}, + "appsmith-bind": {"text": "Java defaults APPSMITH_SERVER_ADDRESS=127.0.0.1 and port 8080 inside the container.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1"], "status": "REASONED"}, + "appsmith-publish": {"text": "Bundled Caddy fronts port 80; development Compose publishes host 8080, AWS example 80/443.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4", "appsmith:sfa551c344e8b"], "status": "REASONED"}, + "appsmith-first": {"text": "First signup claims super-user while no users exist; claim before exposure.", "components": ["appsmith"], "sources": ["appsmith:sd1c55be91e30"], "status": "REASONED"}, + "appsmith-signup": {"text": "APPSMITH_SIGNUP_DISABLED defaults false; set true or restrict APPSMITH_SIGNUP_ALLOWED_DOMAINS. Refusal is SIGNUP_DISABLED.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, + "appsmith-encryption": {"text": "Entrypoint generates 13-character encryption password/salt; external values override them. Development Compose sets both abcd; replace before first start.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, + "appsmith-rotation": {"text": "Changing an existing abcd encryption pair presumably makes stored credentials unreadable; plan re-entry. Inferred, not tested.", "components": ["appsmith"], "sources": ["appsmith:sa68daf87cde7", "appsmith:s9fdcff5a28a4"], "status": "REASONED"}, + "budibase-publish": {"text": "Proxy publishes MAIN_PORT, sample 10000; LiteLLM publishes LITELLM_PORT default 4000.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12"], "status": "REASONED"}, + "budibase-couchdb": {"text": "Proxy /db/ reaches CouchDB. Compose maps COUCH_DB_USER/PASSWORD to COUCHDB_USER/PASSWORD; the image entrypoint adds that admin only when both are non-empty and its existing-admin check finds none for that name. With unchanged samples and that condition, anyone reaching the proxy can try budibase/budibase.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s4d349b4d4ac3", "budibase:s2a033b3a4117", "budibase:scdcd8b6e7071", "budibase:sc74f36aa8340", "budibase:sf6e2a660380f", "budibase:sfdc76e71aff6", "budibase:s851ad716e105"], "status": "REASONED"}, + "budibase-secrets": {"text": "Sample JWT_SECRET/API_ENCRYPTION_KEY are testsecret; service passwords, INTERNAL_API_KEY and LITELLM_MASTER_KEY are budibase. Replace all before first start. Compose pins docker.io/litellm/litellm:main-v1.83.10-stable.patch.3 and passes LITELLM_MASTER_KEY; acceptance of budibase by that image is not established.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "budibase:s5209f2549c9b", "budibase:s32775c45ba51"], "status": "REASONED"}, + "budibase-sample-check": {"text": "This guide's October 2026 search of the full v3.46.0 tag with git grep -l testsecret v3.46.0 found exactly hosting/.env, hosting/digitalocean/files/var/lib/cloud/scripts/per-instance/001_onboot, hosting/hosting.properties, packages/backend-core/tests/jestEnv.ts and scripts/dev/manage.js. This guide inspected those hits. This guide's assessment is that these hits do not establish absence of production rejection; no live safeguard test ran.", "components": ["budibase"], "sources": ["budibase:s735ead81ce9c", "budibase:s5209f2549c9b", "budibase:se993218560de", "budibase:s6179ec0d9abd", "budibase:s9cc27222e133", "budibase:sbe7e0469e7a8"], "status": "REASONED"}, + "budibase-init": {"text": "POST /api/global/users/init is public; its body validator requires string email and tenantId fields, permits an optional password, rejects unknown fields and precedes adminUser. In the supplied tenant context, checkAnyUserExists() truthy causes 403 with the exact vendor error quoted in the body. The helper definition was not fetched, so which users count and any multi-tenancy exception remain unestablished. This guide assesses this POST as unsafe for read-only claimed-state probing: invalid bodies can fail validation in both states, while accepted initialization creates an admin.", "components": ["budibase"], "sources": ["budibase:s45379b35a980", "budibase:sc0a3ac85466f", "budibase:s4902d93986c7", "budibase:s2db467c4f1cf", "budibase:sbd761572fef0", "budibase:sb8f5a18ab4ee", "budibase:s7883460cabbf", "budibase:se18a9c7182fb"], "status": "REASONED"}, + "budibase-seed": {"text": "Claim admin before exposure (guide advice), manually or with BB_ADMIN_USER_EMAIL/BB_ADMIN_USER_PASSWORD. Startup attempts seeding only with SELF_HOSTED truthy, MULTI_TENANCY falsy and both values non-empty; in tenancy.getTenantId() context, it calls createAdminUser when the admin checklist is not checked OR the email does not exist, with hashPassword/requirePassword/skipPasswordValidation true.", "components": ["budibase"], "sources": ["budibase:sc0a3ac85466f", "budibase:sedd5e0682bf0", "budibase:s35ae42bd7a1c", "budibase:s816a78b83956"], "status": "REASONED"}, + "windmill-code": {"text": "Workers execute user scripts; a login enables code execution on worker hosts.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:sd775b7c95872"], "status": "REASONED"}, + "windmill-bind": {"text": "Default 0.0.0.0:8000; SERVER_BIND_ADDR changes binary binding. Default wildcard bind was not observed.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "REASONED"}, + "windmill-compose": {"text": "Compose exposes server 8000 internally; Caddy publishes 80/25 on all host interfaces. Bind publications privately; container loopback cutting Caddy off is inferred.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-sandbox": {"text": "DISABLE_NSJAIL defaults true; sandboxing is not enabled by default.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "REASONED"}, + "windmill-privileged": {"text": "Shipped worker uses privileged=true.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-smtp": {"text": "Port 25 serves email triggers; do not publish unless used.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-debugger": {"text": "Vendor warns REQUIRE_SIGNED_DEBUG_REQUESTS=false exposes an unauthenticated code-execution debugger on reachable deployments.", "components": ["windmill"], "sources": ["windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "verify-nocodb-signup": {"text": "Controlled signup creation is exposed; invite-only refusal is fixed. Hidden links prove nothing; inspect backend setting and confirm admin login.", "components": ["nocodb"], "sources": ["nocodb:s5ea55dc5f0d9", "nocodb:sd0e9f451673f"], "status": "REASONED"}, + "verify-baserow-signup": {"text": "Controlled signup creation is exposed; Sign up is disabled is fixed. Inspect allow_new_signups and confirm admin login.", "components": ["baserow"], "sources": ["baserow:s34b87f60dcc6", "baserow:s4588448964a2"], "status": "REASONED"}, + "verify-appsmith-signup": {"text": "Controlled signup creation is exposed; SIGNUP_DISABLED is fixed. Inspect backend settings, confirm admin login and delete test accounts.", "components": ["appsmith"], "sources": ["appsmith:s2650629933a1", "appsmith:sd1c55be91e30", "appsmith:se3cd894dc579"], "status": "REASONED"}, + "verify-external": {"text": "Probe each public IP; control proves only its port, refusal/timeout may be local filtering, and unexpected connections need host confirmation.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED"}, + "windmill-default-login": {"text": "Fresh loopback DB had only admin@windmill.dev as a password user/super-admin; changeme returned 200/token, then 400 after rotation while the new password returned 200.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac", "windmill:s345ca635c837", "windmill:s03c33b490c08"], "status": "DEMONSTRATED", "evidence": "After the password was changed, `changeme` got `400` and the new password `200`.", "verify": [4]}, + "windmill-loopback": {"text": "SERVER_BIND_ADDR=127.0.0.1 produced only a loopback listener on the test port.", "components": ["windmill"], "sources": ["windmill:s8e4bbba271f2"], "status": "DEMONSTRATED", "evidence": "The server's only listener was on 127.0.0.1, at the test port the run set."}, + "windmill-sandbox-run": {"text": "Loopback worker without nsjail logged sandboxing unavailable; this does not demonstrate isolation.", "components": ["windmill"], "sources": ["windmill:sd775b7c95872"], "status": "DEMONSTRATED", "evidence": "on the loopback run, without nsjail installed, the worker logged \"Nsjail sandboxing will NOT be available\"."}, + "verify-inventory": {"text": "Inventory host sockets and Docker publications; NAT may have no socket, so absent ss entries do not establish isolation.", "components": ["docker", "baserow", "appsmith", "budibase", "windmill"], "sources": ["docker:s2fecb6db5480", "baserow:s1b31314982f4", "appsmith:s9fdcff5a28a4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "REASONED", "verify": [1]}, + "verify-port-display": {"text": "IPv6 wildcard displays [::]: from v27.2.0 versus ::: through v27.1.2; both mean all host addresses unless filtered.", "components": ["docker", "docker-new", "docker-old"], "sources": ["docker:s2fecb6db5480", "docker-new:sca9a0e1e4b6a", "docker-old:sa202ad11d4ed"], "status": "REASONED", "verify": [1]}, + "verify-sample-tokens": {"text": "Whole-token search found sample secrets/variants, clean replaced copies and unreadable paths; harmless matches and external environment values limit scope.", "components": ["nocodb", "appsmith", "budibase", "windmill"], "sources": ["nocodb:s536899ecd4a5", "appsmith:s9fdcff5a28a4", "budibase:s735ead81ce9c", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On copies with the values replaced it printed \"no vendor sample tokens found\", and on an unreadable path it reported that it did not check.", "verify": [2]}, + "verify-nocodb-key": {"text": "docker exec tests configured key presence without printing it or passing its value in argv; no real container run. Startup selection/process value remain unverified.", "components": ["nocodb", "docker"], "sources": ["nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, + "nocodb-key-limits": {"text": "Trusted shells/startup assumed; environment remains readable to same-account/root or Docker-socket holders. Presence does not prove encryption; do not inject a replacement key.", "components": ["nocodb", "docker"], "sources": ["nocodb:sca40040b40cc", "nocodb:sbfbf630008e1", "nocodb:se5da9707c684", "docker:sd34e76a09f6c"], "status": "REASONED", "verify": [3]}, + "nocodb-key-logic": {"text": "A docker exec stand-in exercised set/empty/unset/missing-container branches only; no NocoDB deployment demonstrated.", "components": ["docker"], "sources": ["docker:sd34e76a09f6c"], "status": "DEMONSTRATED", "evidence": "With a stand-in for `docker exec`, the block printed \"non-empty\" for a set key, \"EMPTY or unset\" for an empty and an unset one, and \"not checked\" when the container was missing;"}, + "verify-windmill-control": {"text": "Before trusting default-login 400, confirm real credentials work so a dead service is not treated as fixed.", "components": ["windmill"], "sources": ["windmill:s91439e21d4ac"], "status": "REASONED"}, + "verify-couchdb": {"text": "Sample login at /db/_session?basic=true should return 200 exposed and 401 after replacement; real credentials are the positive control.", "components": ["budibase", "couchdb"], "sources": ["budibase:s735ead81ce9c", "budibase:s7fbf706a4c8f", "couchdb:s34292991c4ec", "budibase:s5209f2549c9b", "budibase:s9afbeb2063b5", "budibase:s2a033b3a4117", "budibase:s851ad716e105"], "status": "REASONED", "verify": [5]}, + "verify-litellm": {"text": "Sample Bearer budibase at /v1/models should return 200 exposed; fixed, a 401 (or a 400 if the proxy has no database); confirm real credentials. The pinned authentication source rejects non-`sk-` keys with 401 when a database is connected and returns 400 without one; the exposed 200 still requires live comparison. These remain comparison expectations: this guide has not established that litellm.md's source pin corresponds to Compose image docker.io/litellm/litellm:main-v1.83.10-stable.patch.3; no source for that image was fetched.", "components": ["budibase", "litellm"], "sources": ["budibase:s735ead81ce9c", "budibase:s74d858053d12", "litellm:sbe6fa0b81017", "budibase:s32775c45ba51"], "status": "REASONED", "verify": [5]}, + "verify-tcp-outcomes": {"text": "Loopback observed connected/open, timeout/full accept queue, refused/closed and failed-control stopping; no real firewall demonstrated.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "On loopback it printed \"connected\" for the control and an open port, \"timed out\" for a port whose accept queue was full (standing in for a filtered one) and \"refused\" for a closed port, and it stopped when the control was closed.", "verify": [6]}, + "verify-tcp-inputs": {"text": "Guard tests reject named hosts, malformed IPv4, zero/dotted IPv6, invalid ports and absent timeout; loopback/hex-mapped IPv6 pass. Invalid hex may fail at control.", "components": ["baserow", "budibase", "windmill"], "sources": ["baserow:s1b31314982f4", "budibase:s74d858053d12", "windmill:sfcc72cf06c9e"], "status": "DEMONSTRATED", "evidence": "`::ffff:0:0` and `::ffff:7f00:1` passed and connected to a listener bound to 127.0.0.1", "verify": [6]} + } +} +```` +====================================================================== +==> server-admin-panels.md +====================================================================== + +# Server administration panels: Cockpit, Webmin, and Proxmox VE + +Written against Cockpit 368, Webmin 2.670, Proxmox VE 9.2 docs 9.2.12, pve-manager 9.2.20, Usermin 2.570, and systemd v262; 3 further sources are pinned to a commit. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 38 claims: 4 demonstrated by a check that ran and 34 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 server administration panel is root on the host behind a login form. Cockpit gives any user who can escalate with sudo or polkit an administrative session and a terminal. Webmin's packages make the host's root password a Webmin login with every module, including a root shell. Proxmox VE's web interface is the @@ -54445,6 +58104,227 @@ give the server's public address. A "connected" on a port you did not mean to ex - libpve-http-server-perl 6.0.5 configuration reader, lines 10 to 97, at 5119ff9bec08c69584c0c98bea3edd0098179e5f (checked October 2026): https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm - libpve-common-perl 9.1.21 socket creation and reuse, lines 803 to 854, at 5054082fe492429fc37574985c2ca812af9a3125 (checked October 2026): https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm + + +## 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/server-admin-panels.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): + +- cockpit: Cockpit (368) +- webmin: Webmin (2.670) +- pve-docs: Proxmox VE 9.2 docs (9.2.12) +- pve-manager: pve-manager (9.2.20) +- webmin-docs: Webmin docs (8ceae26c5a074053905cbcc6c0053be573633f3a) +- pve-http: Proxmox HTTP server (5119ff9bec08c69584c0c98bea3edd0098179e5f) +- pve-common: libpve-common-perl 9.1.21 (5054082fe492429fc37574985c2ca812af9a3125) +- usermin: Usermin (2.570) +- systemd: systemd (v262) + +Claims (full source mapping in the JSON block below): + +- private (REASONED; cockpit, webmin-docs, pve-docs): Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor. +- cockpit-bind (REASONED; cockpit, systemd): Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses. +- cockpit-dropin (REASONED; cockpit): Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart. +- cockpit-tls (REASONED; cockpit): One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation. +- cockpit-pam (REASONED; cockpit): Local accounts use Cockpit PAM with normal SSH-equivalent privileges. +- cockpit-root (REASONED; cockpit): First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable. +- cockpit-escalation (REASONED; cockpit): Users allowed sudo/polkit escalation get an immediately elevated session and terminal. +- cockpit-mfa (REASONED; cockpit): No built-in second factor; use PAM, Kerberos or client certificates. +- cockpit-rate (REASONED; cockpit): MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout. +- cockpit-loginto (REASONED; cockpit): Set LoginTo=false to prevent unauthenticated scanning of reachable private networks. +- webmin-bind (REASONED; webmin, usermin): Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses. +- webmin-discovery (REASONED; webmin, usermin): Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused. +- webmin-access (REASONED; webmin, webmin-docs): Set management bind and allow networks, then restart; empty allow admits all except denied clients. +- webmin-tls (REASONED; webmin): Deb/RPM packages enable ssl=1 with a self-signed certificate. +- webmin-root (REASONED; webmin): Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root. +- webmin-sudo (REASONED; webmin): Installed sudo=1 lets sudo-permitted Unix users log in effectively as root. +- webmin-mfa (REASONED; webmin): TOTP/Authy exists but is not configured at install; enable and enrol every user. +- webmin-advisories (REASONED; webmin-docs): Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release. +- webmin-rate (REASONED; webmin): Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1. +- pve-bind (REASONED; pve-docs, pve-manager, pve-http, pve-common): pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified. +- pve-cluster (REASONED; pve-docs): The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules. +- pve-acl (REASONED; pve-docs, pve-manager, pve-http): Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone. +- pve-firewall (REASONED; pve-docs): Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses. +- pve-root (REASONED; pve-docs): root@pam can always log in as unconfined admin; installer sets the web-interface root password. +- pve-shell (REASONED; pve-manager): Node shell opens /bin/login -f root without another password. +- pve-mfa (REASONED; pve-docs): Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor. +- pve-lockout (REASONED; pve-docs): Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts. +- pve-password-delay (REASONED; pve-http): Unauthorized API responses delay three seconds; this is not password lockout. +- pve-certificates (REASONED; pve-docs): Each cluster creates its own self-signed CA. +- pve-tokens (REASONED; pve-docs): API tokens default to separated privileges and cannot access VM/node consoles. +- verify-cockpit (REASONED; cockpit): Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults. +- verify-pve (REASONED; pve-docs, pve-manager, pve-http, pve-common): Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings. +- verify-external (REASONED; cockpit, webmin, usermin, pve-docs): Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP. +- webmin-loopback (DEMONSTRATED; webmin): Loopback bind with no listen line produced one TCP socket and no UDP. +- webmin-rate-run (DEMONSTRATED; webmin): Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403. +- verify-listeners (REASONED; cockpit, webmin, usermin, pve-docs, pve-manager, pve-http, pve-common): Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host. +- verify-webmin-parser (DEMONSTRATED; webmin): Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof. +- verify-tcp-loopback (DEMONSTRATED; cockpit, webmin, usermin, pve-docs): Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "3478bd2ff9b8156ba6721d2d02631c39a01142288dfa2d17f860b649772c131f", + "components": { + "cockpit": { + "name": "Cockpit", + "basis": "368", + "sources": { + "sc42ba09017c7": "https://github.com/cockpit-project/cockpit/blob/368/src/systemd/cockpit.socket.in", + "s0c1e0db624a4": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/listen.adoc", + "sd0cb4ea3db56": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/https.adoc", + "sf157ab3a6f47": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/man/pages/cockpit.conf.5.adoc", + "s48ebdec28e10": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/authentication.adoc", + "s2432e599485f": "https://github.com/cockpit-project/cockpit/blob/368/doc/modules/guide/pages/privileges.adoc", + "sbdcd6e74a0c7": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.pam", + "sc20103cfb39b": "https://github.com/cockpit-project/cockpit/blob/368/tools/cockpit.spec" + } + }, + "webmin": { + "name": "Webmin", + "basis": "2.670", + "sources": { + "sa4f7ac343e85": "https://github.com/webmin/webmin/blob/2.670/setup.sh", + "sb8fec2baa0f2": "https://github.com/webmin/webmin/blob/2.670/miniserv.pl", + "sdd465cac1ff8": "https://github.com/webmin/webmin/blob/2.670/makedebian.pl", + "s9ecba8f15014": "https://github.com/webmin/webmin/blob/2.670/makerpm.pl", + "sdb2ef2bf9978": "https://github.com/webmin/webmin/blob/2.670/miniserv-lib.pl", + "s1a1dbd21dddd": "https://github.com/webmin/webmin/blob/2.670/shell/defaultacl", + "s2f3504ad1328": "https://github.com/webmin/webmin/blob/2.670/webmin/twofactor-funcs-lib.pl" + } + }, + "pve-docs": { + "name": "Proxmox VE 9.2 docs", + "basis": "9.2.12", + "sources": { + "s8538bab3d3fc": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveproxy.adoc", + "sdbc681682e8f": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pveum.adoc", + "sc6bbbfe64d09": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=pve-firewall.adoc", + "s31f242f10eaa": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=certificate-management.adoc", + "s954445cf4e14": "https://git.proxmox.com/?p=pve-docs.git;a=blob_plain;hb=9370638116430c4b1ccb9707b5716eaad7c7c9d3;f=debian/changelog" + } + }, + "pve-manager": { + "name": "pve-manager", + "basis": "9.2.20", + "sources": { + "se4c11b901cf8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/API2/Nodes.pm", + "s1507ce3e40b8": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/changelog", + "s67c1a8621e22": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=debian/control", + "sb7f08e4e98ab": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/pveproxy.pm", + "s0b79b5bbd95f": "https://git.proxmox.com/?p=pve-manager.git;a=blob_plain;hb=49318c671b82f31e6b273b79447526161739b97a;f=PVE/Service/spiceproxy.pm" + } + }, + "webmin-docs": { + "name": "Webmin docs", + "basis": "8ceae26c5a074053905cbcc6c0053be573633f3a", + "sources": { + "s7a91f9f23044": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/docs/Modules/webmin-configuration.md", + "s8b072a3403d1": "https://github.com/webmin/webmin.com/blob/8ceae26c5a074053905cbcc6c0053be573633f3a/content/security.md" + } + }, + "pve-http": { + "name": "Proxmox HTTP server", + "basis": "5119ff9bec08c69584c0c98bea3edd0098179e5f", + "sources": { + "s753d88abe320": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/AnyEvent.pm", + "sb8d499c6bd8c": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=debian/changelog", + "s2f151d3facd0": "https://git.proxmox.com/?p=pve-http-server.git;a=blob_plain;hb=5119ff9bec08c69584c0c98bea3edd0098179e5f;f=src/PVE/APIServer/Utils.pm" + } + }, + "pve-common": { + "name": "libpve-common-perl 9.1.21", + "basis": "5054082fe492429fc37574985c2ca812af9a3125", + "sources": { + "s062026297433": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=debian/changelog", + "s022a9e4640ab": "https://git.proxmox.com/?p=pve-common.git;a=blob_plain;hb=5054082fe492429fc37574985c2ca812af9a3125;f=src/PVE/Daemon.pm" + } + }, + "usermin": { + "name": "Usermin", + "basis": "2.570", + "sources": { + "s7fe74c9a3138": "https://github.com/webmin/usermin/blob/2.570/setup.sh" + } + }, + "systemd": { + "name": "systemd", + "basis": "v262", + "sources": { + "s0a96457c713e": "https://github.com/systemd/systemd/blob/v262/man/systemd.socket.xml" + } + } + }, + "claims": { + "private": {"text": "Keep root-capable panels off the internet, bound/restricted to management networks with private access and a second factor.", "components": ["cockpit", "webmin-docs", "pve-docs"], "sources": ["cockpit:s0c1e0db624a4", "webmin-docs:s7a91f9f23044", "pve-docs:s8538bab3d3fc", "pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, + "cockpit-bind": {"text": "Bare ListenStream=9090 binds IPv6 and, by default, IPv4 wildcard addresses.", "components": ["cockpit", "systemd"], "sources": ["cockpit:sc42ba09017c7", "systemd:s0a96457c713e"], "status": "REASONED"}, + "cockpit-dropin": {"text": "Use a socket drop-in, not cockpit.conf: reset ListenStream, set 10.0.0.5:9090 and FreeBind=yes, then daemon-reload/restart.", "components": ["cockpit"], "sources": ["cockpit:s0c1e0db624a4"], "status": "REASONED"}, + "cockpit-tls": {"text": "One port serves HTTP/HTTPS, redirecting HTTP except localhost; AllowUnencrypted defaults false and missing certificates cause self-signed creation.", "components": ["cockpit"], "sources": ["cockpit:sd0cb4ea3db56", "cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "cockpit-pam": {"text": "Local accounts use Cockpit PAM with normal SSH-equivalent privileges.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, + "cockpit-root": {"text": "First-install packages write root to disallowed-users; onerr=succeed permits root if that file is missing/unreadable.", "components": ["cockpit"], "sources": ["cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, + "cockpit-escalation": {"text": "Users allowed sudo/polkit escalation get an immediately elevated session and terminal.", "components": ["cockpit"], "sources": ["cockpit:s2432e599485f"], "status": "REASONED"}, + "cockpit-mfa": {"text": "No built-in second factor; use PAM, Kerberos or client certificates.", "components": ["cockpit"], "sources": ["cockpit:s48ebdec28e10"], "status": "REASONED"}, + "cockpit-rate": {"text": "MaxStartups defaults to 10 concurrent attempts, not repeated-password lockout.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "cockpit-loginto": {"text": "Set LoginTo=false to prevent unauthenticated scanning of reachable private networks.", "components": ["cockpit"], "sources": ["cockpit:sf157ab3a6f47"], "status": "REASONED"}, + "webmin-bind": {"text": "Webmin/Usermin TCP ports are 10000/20000; omitted bind makes miniserv listen on all IPv4/IPv6 addresses.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, + "webmin-discovery": {"text": "Installed listen=10000/20000 opens discovery UDP on every IPv4 address; delete when unused.", "components": ["webmin", "usermin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138"], "status": "REASONED"}, + "webmin-access": {"text": "Set management bind and allow networks, then restart; empty allow admits all except denied clients.", "components": ["webmin", "webmin-docs"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978", "webmin-docs:s7a91f9f23044"], "status": "REASONED"}, + "webmin-tls": {"text": "Deb/RPM packages enable ssl=1 with a self-signed certificate.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdd465cac1ff8", "webmin:s9ecba8f15014"], "status": "REASONED"}, + "webmin-root": {"text": "Installed root crypt=x accepts the Unix root password with all modules; Command Shell defaults to root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:s1a1dbd21dddd"], "status": "REASONED"}, + "webmin-sudo": {"text": "Installed sudo=1 lets sudo-permitted Unix users log in effectively as root.", "components": ["webmin"], "sources": ["webmin:sdd465cac1ff8", "webmin:s9ecba8f15014", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, + "webmin-mfa": {"text": "TOTP/Authy exists but is not configured at install; enable and enrol every user.", "components": ["webmin"], "sources": ["webmin:s2f3504ad1328"], "status": "REASONED"}, + "webmin-advisories": {"text": "Advisories list Basic-auth MFA bypasses CVE-2026-42210/CVE-2026-56022 under Webmin prior to 2.641; use a current release.", "components": ["webmin-docs"], "sources": ["webmin-docs:s8b072a3403d1"], "status": "REASONED"}, + "webmin-rate": {"text": "Defaults blockhost_failures=5/blockhost_time=60 block a host for 60 seconds, not a user lockout; Basic-auth failures count only with installed passdelay=1.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "REASONED"}, + "pve-bind": {"text": "pveproxy HTTPS API 8006 and spiceproxy 3128 share LISTEN_IP in /etc/default/pveproxy; new sockets without it try :: then 0.0.0.0. Dual-stack defaults and bindv6only=1 effects are documentation-backed; socket-library behaviour and installed versions remain unverified.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab"], "status": "REASONED"}, + "pve-cluster": {"text": "The vendor warns against LISTEN_IP on clustered systems: nodes need peer pveproxy access, possibly across subnets. This guide advises management-only ACLs and firewall rules.", "components": ["pve-docs"], "sources": ["pve-docs:s8538bab3d3fc"], "status": "REASONED"}, + "pve-acl": {"text": "Both daemons share ALLOW_FROM/DENY_FROM/POLICY; default allow admits unmatched clients and dual matches. Deny rejects both cases. This guide advises ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, because deny plus DENY_FROM=all refuses everyone.", "components": ["pve-docs", "pve-manager", "pve-http"], "sources": ["pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-http:s753d88abe320"], "status": "REASONED"}, + "pve-firewall": {"text": "Firewall defaults disabled; enable and restrict 8006/22/3128 to management addresses.", "components": ["pve-docs"], "sources": ["pve-docs:sc6bbbfe64d09"], "status": "REASONED"}, + "pve-root": {"text": "root@pam can always log in as unconfined admin; installer sets the web-interface root password.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-shell": {"text": "Node shell opens /bin/login -f root without another password.", "components": ["pve-manager"], "sources": ["pve-manager:se4c11b901cf8"], "status": "REASONED"}, + "pve-mfa": {"text": "Configure TOTP, WebAuthn, recovery keys or realm-enforced TOTP/YubiKey OTP; no default second factor.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-lockout": {"text": "Eight TOTP failures disable TOTP; 100 WebAuthn/recovery-key failures block all factors for an hour. These are second-factor lockouts.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "pve-password-delay": {"text": "Unauthorized API responses delay three seconds; this is not password lockout.", "components": ["pve-http"], "sources": ["pve-http:s753d88abe320"], "status": "REASONED"}, + "pve-certificates": {"text": "Each cluster creates its own self-signed CA.", "components": ["pve-docs"], "sources": ["pve-docs:s31f242f10eaa"], "status": "REASONED"}, + "pve-tokens": {"text": "API tokens default to separated privileges and cannot access VM/node consoles.", "components": ["pve-docs"], "sources": ["pve-docs:sdbc681682e8f"], "status": "REASONED"}, + "verify-cockpit": {"text": "Inspect effective socket reset/address and root deny file; bare port or missing deny file exposes documented defaults.", "components": ["cockpit"], "sources": ["cockpit:sc42ba09017c7", "cockpit:s0c1e0db624a4", "cockpit:sbdcd6e74a0c7", "cockpit:sc20103cfb39b"], "status": "REASONED"}, + "verify-pve": {"text": "Inspect the shared LISTEN_IP/ACL configuration for both daemons, enabled/running firewall and management-only rules, and every admin's second factor including root@pam. This guide assesses management bind on a single node or management ALLOW_FROM with DENY_FROM=all or POLICY=deny, one of the two, not both, as fixed; grep alone does not establish effective settings.", "components": ["pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["pve-docs:s8538bab3d3fc", "pve-docs:sdbc681682e8f", "pve-docs:sc6bbbfe64d09", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED"}, + "verify-external": {"text": "Probe each public IP with a reachable control; refusal/timeout is only consistent with isolation. TCP does not test Webmin UDP.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "REASONED"}, + "webmin-loopback": {"text": "Loopback bind with no listen line produced one TCP socket and no UDP.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2"], "status": "DEMONSTRATED", "evidence": "its only socket was TCP `127.0.0.1` on the test port, with no UDP socket."}, + "webmin-rate-run": {"text": "Fifth wrong password exceeded a five-second timeout; then even the right password received host-block 403.", "components": ["webmin"], "sources": ["webmin:sa4f7ac343e85", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "the fifth wrong password was delayed past a five-second client timeout by miniserv's growing failure delay, and the right password was then refused with `403` \"Access denied for 127.0.0.1\"."}, + "verify-listeners": {"text": "Expect management TCP binds and no discovery UDP; this guide assesses clustered Proxmox wildcards as fixed only with management-only ACLs and firewall rules. Pinned new-socket code tries :: then 0.0.0.0; dual-stack operation remains documentation-backed and must be checked on the host.", "components": ["cockpit", "webmin", "usermin", "pve-docs", "pve-manager", "pve-http", "pve-common"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc", "pve-manager:s1507ce3e40b8", "pve-docs:s954445cf4e14", "pve-http:sb8d499c6bd8c", "pve-common:s062026297433", "pve-manager:s67c1a8621e22", "pve-manager:sb7f08e4e98ab", "pve-manager:s0b79b5bbd95f", "pve-http:s2f151d3facd0", "pve-common:s022a9e4640ab", "pve-http:s753d88abe320"], "status": "REASONED", "verify": [1]}, + "verify-webmin-parser": {"text": "Thirty fixtures matched miniserv parsing and bind/sockets/discovery/ACL outputs; unreadable paths were not checked. File inspection is not running-state proof.", "components": ["webmin"], "sources": ["webmin:sb8fec2baa0f2", "webmin:sdb2ef2bf9978"], "status": "DEMONSTRATED", "evidence": "On each, the copy's parsed values matched Webmin 2.670's own `read_config_file` run on the same file, and the block printed the outcomes above; it reported an unreadable path as not checked.", "verify": [2]}, + "verify-tcp-loopback": {"text": "Copied probe has recorded loopback provenance, not live panel-isolation evidence; loopback and mapped hex addresses pass its guard.", "components": ["cockpit", "webmin", "usermin", "pve-docs"], "sources": ["cockpit:sc42ba09017c7", "webmin:sb8fec2baa0f2", "usermin:s7fe74c9a3138", "pve-docs:s8538bab3d3fc"], "status": "DEMONSTRATED", "evidence": "loopback addresses pass, and hex spellings such as `::ffff:0:0` connected to a listener bound to 127.0.0.1", "verify": [3]} + } +} +```` + + ====================================================================== ==> nomad-consul.md ====================================================================== diff --git a/sqlite-http-frontends.md b/sqlite-http-frontends.md index 0453b1cf9..5155c56c4 100644 --- a/sqlite-http-frontends.md +++ b/sqlite-http-frontends.md @@ -1,338 +1,12 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-09-27", - "documentation_checked": "2026-09", - "body_sha256": "cd2fba66ff332b791a1934a25ece6c5ef7f76fc791a8a4019819189f360eab0b", - "components": { - "ds-container": { - "name": "Datasette container generator", - "basis": "caf238aac86ebe370959b25384d6e05f6a1e2359", - "sources": { - "s3c9be24424e0": "https://raw.githubusercontent.com/simonw/datasette/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py", - "sffebbc2c5528": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L576-L655", - "seae12396d87a": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L658-L736", - "s9e735f75bf4b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L173-L179", - "s224af1264bb1": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L203-L306", - "sf0ea47721a46": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L352-L358", - "sae5997e91b6b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/cloudrun.py#L17-L244", - "s735b0ff17b61": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/heroku.py#L21-L254", - "s2484732fbf31": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/tests/test_package.py#L11-L61" - } - }, - "ds-cloud": { - "name": "Datasette Cloud Run publisher", - "basis": "e889403d3bbe143854262682161c98a57bdb6594", - "sources": { - "sf5859249ea9f": "https://github.com/simonw/datasette/blob/e889403d3bbe143854262682161c98a57bdb6594/datasette/publish/cloudrun.py" - } - }, - "fly": { - "name": "Datasette Fly publishing plugin", - "basis": "unknown", - "sources": { - "s2a2db4192225": "https://github.com/simonw/datasette-publish-fly" - } - }, - "passwords": { - "name": "datasette-auth-passwords", - "basis": "1.1.1", - "sources": { - "sb6597be5af67": "https://github.com/simonw/datasette-auth-passwords/blob/1.1.1/README.md" - } - }, - "github-auth": { - "name": "datasette-auth-github", - "basis": "unknown", - "sources": { - "s90107b903580": "https://github.com/simonw/datasette-auth-github" - } - }, - "write": { - "name": "datasette-write", - "basis": "0.4", - "sources": { - "s073e10d21091": "https://github.com/simonw/datasette-write/blob/0.4/README.md" - } - }, - "web": { - "name": "sqlite-web package", - "basis": "0.8.1", - "sources": { - "s91f8591a5f84": "https://github.com/coleifer/sqlite-web/blob/0.8.1/README.md" - } - }, - "web-source": { - "name": "sqlite-web implementation", - "basis": "825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2", - "sources": { - "s13d71c0a8cb2": "https://raw.githubusercontent.com/coleifer/sqlite-web/825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2/sqlite_web/sqlite_web.py" - } - }, - "web-tag-source": { - "name": "sqlite-web tagged implementation", - "basis": "0.8.1", - "sources": { - "s1626cd0d975f": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L3", - "s8c12f9aab57c": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1735-L1740", - "s30bba3ce63c6": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1944-L1956" - } - }, - "web-image": { - "name": "sqlite-web Dockerfile", - "basis": "7f6658b012a789f0b9418cad543e7798ce850130", - "sources": { - "s4c81e6becc20": "https://raw.githubusercontent.com/coleifer/sqlite-web/7f6658b012a789f0b9418cad543e7798ce850130/docker/Dockerfile" - } - }, - "sqlite": { - "name": "SQLite documentation", - "basis": "unknown", - "sources": { - "s4e4e320abf5b": "https://www.sqlite.org/uri.html", - "s2721da91f8d2": "https://www.sqlite.org/serverless.html", - "sadf0431c1271": "https://www.sqlite.org/security.html" - } - }, - "docker": { - "name": "Docker localhost-publication boundary", - "basis": "28.0.0", - "sources": { - "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" - } - }, - "cloudrun": { - "name": "Cloud Run documentation", - "basis": "unknown", - "sources": { - "sb90a6c13388a": "https://docs.cloud.google.com/run/docs/container-contract" - } - }, - "nginx": { - "name": "nginx documentation", - "basis": "unknown", - "sources": { - "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", - "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html" - } - }, - "iap": { - "name": "Google IAP documentation", - "basis": "unknown", - "sources": { - "saafdfc163fce": "https://docs.cloud.google.com/iap/docs/concepts-overview" - } - }, - "access": { - "name": "Cloudflare Access documentation", - "basis": "unknown", - "sources": { - "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/", - "s06747cf7388c": "https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/independent-mfa/" - } - }, - "tailscale": { - "name": "Tailscale documentation", - "basis": "unknown", - "sources": { - "s5b9a9b15ae80": "https://tailscale.com/docs/reference/tailscale-cli/serve" - } - }, - "curl": { - "name": "curl minimum write-out version", - "basis": "7.75.0", - "sources": { - "s2b2686afaf41": "https://curl.se/docs/manpage.html" - } - }, - "ss": { - "name": "ss documentation", - "basis": "unknown", - "sources": { - "sfc58f455acfa": "https://man7.org/linux/man-pages/man8/ss.8.html" - } - }, - "datasette-docs": { - "name": "Datasette documentation (rolling)", - "basis": "unknown", - "sources": { - "s3e64933fe2e9": "https://docs.datasette.io/en/stable/changelog.html", - "sb0aae2446988": "https://docs.datasette.io/en/stable/cli-reference.html", - "sca59b58c5c84": "https://docs.datasette.io/en/stable/performance.html", - "s8f2fe7a13b3f": "https://docs.datasette.io/en/stable/installation.html", - "se7cc29f79be8": "https://docs.datasette.io/en/stable/publish.html", - "s878851fd8eac": "https://docs.datasette.io/en/stable/authentication.html", - "s824eb176fda7": "https://docs.datasette.io/en/stable/sql_queries.html", - "s406cc998a5da": "https://docs.datasette.io/en/stable/internals.html#csrf-protection", - "se154b0fa47dd": "https://docs.datasette.io/en/stable/settings.html", - "s96c8e6562139": "https://docs.datasette.io/en/stable/json_api.html", - "s1e93b280c5af": "https://docs.datasette.io/en/stable/introspection.html", - "sb1b30f98f026": "https://docs.datasette.io/en/stable/plugins.html#secret-configuration-values", - "s53e84082f6d1": "https://docs.datasette.io/en/stable/deploying.html" - } - }, - "caddy-docs": { - "name": "Caddy documentation (rolling)", - "basis": "unknown", - "sources": { - "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth" - } - } - }, - "claims": { - "release": {"text": "The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9"], "status": "REASONED"}, - "datasette-bind": {"text": "Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "DEMONSTRATED", "evidence": "bare `datasette serve` with no host or port flag listened on `127.0.0.1:8001`"}, - "immutable": {"text": "Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors.", "components": ["sqlite", "datasette-docs"], "sources": ["datasette-docs:sca59b58c5c84", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, - "datasette-docker": {"text": "Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate.", "components": ["datasette-docs"], "sources": ["datasette-docs:s8f2fe7a13b3f"], "status": "REASONED"}, - "container-generator": {"text": "At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address.", "components": ["ds-container"], "sources": ["ds-container:s3c9be24424e0", "ds-container:sffebbc2c5528", "ds-container:seae12396d87a", "ds-container:s9e735f75bf4b", "ds-container:s224af1264bb1", "ds-container:sf0ea47721a46", "ds-container:sae5997e91b6b", "ds-container:s735b0ff17b61", "ds-container:s2484732fbf31"], "status": "REASONED"}, - "cloudrun": {"text": "Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization.", "components": ["ds-cloud", "cloudrun"], "sources": ["ds-cloud:sf5859249ea9f", "cloudrun:sb90a6c13388a"], "status": "REASONED"}, - "publish-plugins": {"text": "Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established.", "components": ["fly", "datasette-docs"], "sources": ["datasette-docs:se7cc29f79be8", "fly:s2a2db4192225"], "status": "REASONED"}, - "docker-publication": {"text": "Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally.", "components": ["docker"], "sources": ["docker:s1e53417c513d"], "status": "REASONED"}, - "actor-policy": {"text": "Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it.", "components": ["github-auth", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "github-auth:s90107b903580"], "status": "REASONED"}, - "root-url": {"text": "datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:sb0aae2446988"], "status": "REASONED"}, - "canned-writes": {"text": "Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode.", "components": ["datasette-docs"], "sources": ["datasette-docs:s824eb176fda7"], "status": "REASONED"}, - "write-plugin": {"text": "datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "REASONED"}, - "sql-policy": {"text": "?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, - "query-limits": {"text": "Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, - "downloads": {"text": "allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd"], "status": "DEMONSTRATED", "evidence": "`/published.db` returned `403`; without `--setting allow_download off` it returned `200`."}, - "cors": {"text": "The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "DEMONSTRATED", "evidence": "`--cors` added `Access-Control-Allow-Origin: *`, absent without it."}, - "introspection": {"text": "/-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission.", "components": ["datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, - "hash-urls": {"text": "Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9", "datasette-docs:sca59b58c5c84"], "status": "REASONED"}, - "csrf": {"text": "Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads.", "components": ["datasette-docs"], "sources": ["datasette-docs:s406cc998a5da"], "status": "REASONED"}, - "plugin-secrets": {"text": "The local analyst saw inline password hashes in metadata, but only $env references after environment indirection.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "`/-/metadata.json` showed that analyst both users' password hashes when they were written inline in the plugin configuration, but only the `$env` references when they were supplied through environment variables."}, - "web-bind": {"text": "Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "bare `sqlite_web` on `127.0.0.1:8080`, as `ss -tlnp` run without `sudo` by the same account showed"}, - "web-capabilities": {"text": "sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, - "web-container": {"text": "sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy.", "components": ["web-image", "web"], "sources": ["web-image:s4c81e6becc20", "web:s91f8591a5f84"], "status": "REASONED"}, - "web-password": {"text": "Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "`SQLITE_WEB_PASSWORD` set without `-P` left sqlite-web open (`200` anonymously), as the source reading above says; with `-P` the anonymous query GET got `302` to `/login/`."}, - "web-readonly": {"text": "In the local --read-only run the anonymous read still returned 200 while the insert changed nothing.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "DEMONSTRATED", "evidence": "with `--read-only` the read still returned `200` and the insert changed nothing."}, - "web-methods": {"text": "Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access.", "components": ["web-source"], "sources": ["web-source:s13d71c0a8cb2"], "status": "REASONED"}, - "web-tls": {"text": "sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, - "web-csrf": {"text": "sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "an authenticated `POST` insert with no token and a foreign `Origin` header wrote a row, and the insert form contains no CSRF field."}, - "web-cookie": {"text": "sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Its session cookie was set with `HttpOnly; Path=/` and no `SameSite` or `Secure` attribute, even over TLS"}, - "fronting": {"text": "Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route.", "components": ["nginx", "caddy-docs", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "nginx:s6a0728ca7609", "nginx:sf32ce2956917", "caddy-docs:sbc2bd9599bd1"], "status": "REASONED"}, - "identity-proxy": {"text": "IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes.", "components": ["iap", "access"], "sources": ["iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED"}, - "mfa": {"text": "Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA.", "components": ["web", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "access:s06747cf7388c"], "status": "REASONED"}, - "tailnet": {"text": "Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured.", "components": ["tailscale"], "sources": ["tailscale:s5b9a9b15ae80"], "status": "REASONED"}, - "origin-bypass": {"text": "Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions.", "components": ["iap"], "sources": ["iap:saafdfc163fce"], "status": "REASONED"}, - "os-account": {"text": "Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority.", "components": ["sqlite"], "sources": ["sqlite:s2721da91f8d2", "sqlite:sadf0431c1271"], "status": "REASONED"}, - "verify-inventory": {"text": "Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability.", "components": ["web", "ss", "docker", "cloudrun", "datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "web:s91f8591a5f84", "ss:sfc58f455acfa", "docker:s1e53417c513d", "cloudrun:sb90a6c13388a"], "status": "REASONED", "verify": [1]}, - "verify-external": {"text": "External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available.", "components": ["web", "iap", "curl", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "web:s91f8591a5f84", "iap:saafdfc163fce", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, - "verify-anonymous": {"text": "UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control.", "components": ["web", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:s824eb176fda7", "web:s91f8591a5f84"], "status": "REASONED", "verify": [2]}, - "datasette-exposed": {"text": "Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "datasette-docs:se154b0fa47dd", "datasette-docs:s96c8e6562139", "datasette-docs:s1e93b280c5af"], "status": "DEMONSTRATED", "evidence": "Against the baseline command above, the SQL JSON route returned `403` while the table's JSON page still returned `200` (SQL disabled, browsing permitted)"}, - "web-exposed": {"text": "Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Bare `sqlite_web` answered the query GET with `200` anonymously, and an anonymous `POST` insert wrote a row to a disposable database"}, - "verify-authorized": {"text": "Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy.", "components": ["web", "curl", "iap", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "curl:s2b2686afaf41", "iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED", "verify": [3]}, - "actor-results": {"text": "Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "the block returned the table's rows to the `analyst` session cookie (`authorized http=200`) and `403` anonymously; an authenticated `guest` got `403`, the unauthorized outcome, and the analyst got `403` on SQL under `allow_sql: false`."}, - "web-auth-results": {"text": "Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "For sqlite-web with `-P`, the session cookie got the row with `200` and the anonymous request `302` to `/login/`."}, - "introspection-results": {"text": "Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "Under the same metadata, `/-/versions.json`, `/-/metadata.json`, `/-/plugins.json`, `/-/settings.json` and `/-/databases.json` returned `403` anonymously and `200` to a freshly logged-in analyst"}, - "caddy-results": {"text": "Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation.", "components": ["caddy-docs", "datasette-docs"], "sources": ["caddy-docs:sbc2bd9599bd1", "datasette-docs:s53e84082f6d1"], "status": "DEMONSTRATED", "evidence": "Block C through the proxy gave `authorized http=200` with the rows and `anonymous http=401` with `www-authenticate: Basic realm=\"restricted\"`."}, - "local-checks": {"text": "Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Local authoring checks passed: `bash -n` on all five shell blocks, parsing of the metadata JSON, and 120 guard executions covering 40 cases under ordinary Bash, `set -u`, and `set -u` with `IFS=0`."}, - "guard-limits": {"text": "Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Across 48 block B/C cases under ordinary Bash, set -u, and set -u with IFS=0, an explicitly substituted curl argument recorder ran with valid whole-block values, inherited matching marker/arguments, and pastes below the guards. Whole-block placeholders and omitted assignments without a matching marker exited 2 without calling it."}, - "datasette-cli": {"text": "Bare datasette invokes serve; -h/--host and -p/--port override its bind and port.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "REASONED"}, - "web-cli": {"text": "sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "REASONED"}, - "write-test": {"text": "Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "DEMONSTRATED", "evidence": "With datasette-write on a disposable database, an anonymous write got `403`, a root write without the CSRF token `403`, and with the token `302`, and the row was written."}, - "secret-indirection": {"text": "Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, - "web-auth-config": {"text": "In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles.", "components": ["web", "web-source", "web-tag-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "web-tag-source:s1626cd0d975f", "web-tag-source:s8c12f9aab57c", "web-tag-source:s30bba3ce63c6"], "status": "REASONED"}, - "web-readonly-config": {"text": "--read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, - "cors-policy": {"text": "--cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "REASONED"}, - "query-pages": {"text": "Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"} - } -} ---- # SQLite HTTP front-ends: Datasette and sqlite-web - -**Version basis** +Written against Datasette container generator (pinned to a commit), sqlite-web package 0.8.1, sqlite-web tagged implementation 0.8.1, datasette-auth-passwords 1.1.1, datasette-write 0.4, Docker localhost-publication boundary 28.0.0, and curl minimum write-out version 7.75.0; 3 further sources are pinned to a commit and 11 carry no recorded version. Metadata reviewed 2026-09-27; documentation checked September 2026. The guide records 55 claims: 18 demonstrated by a check that ran and 37 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. +An SQLite HTTP front-end turns a local file into a network service: **the port is the exposure**. Datasette allows anonymous browsing and read-only SQL by default; sqlite-web provides queries, imports, exports, row changes, and schema deletion. An unprotected, reachable instance exposes those capabilities. See Datasette permissions and sqlite-web features. -Components (recorded basis): - -- ds-container: Datasette container generator (caf238aac86ebe370959b25384d6e05f6a1e2359) -- ds-cloud: Datasette Cloud Run publisher (e889403d3bbe143854262682161c98a57bdb6594) -- fly: Datasette Fly publishing plugin (unknown) -- passwords: datasette-auth-passwords (1.1.1) -- github-auth: datasette-auth-github (unknown) -- write: datasette-write (0.4) -- web: sqlite-web package (0.8.1) -- web-source: sqlite-web implementation (825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2) -- web-tag-source: sqlite-web tagged implementation (0.8.1) -- web-image: sqlite-web Dockerfile (7f6658b012a789f0b9418cad543e7798ce850130) -- sqlite: SQLite documentation (unknown) -- docker: Docker localhost-publication boundary (28.0.0) -- cloudrun: Cloud Run documentation (unknown) -- nginx: nginx documentation (unknown) -- iap: Google IAP documentation (unknown) -- access: Cloudflare Access documentation (unknown) -- tailscale: Tailscale documentation (unknown) -- curl: curl minimum write-out version (7.75.0) -- ss: ss documentation (unknown) -- datasette-docs: Datasette documentation (rolling) (unknown) -- caddy-docs: Caddy documentation (rolling) (unknown) - -Claims (full source mapping in the front matter): - -- release (REASONED; datasette-docs): The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes. -- datasette-bind (DEMONSTRATED; datasette-docs): Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory. -- immutable (REASONED; sqlite, datasette-docs): Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors. -- datasette-docker (REASONED; datasette-docs): Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate. -- container-generator (REASONED; ds-container): At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address. -- cloudrun (REASONED; ds-cloud, cloudrun): Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization. -- publish-plugins (REASONED; fly, datasette-docs): Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established. -- docker-publication (REASONED; docker): Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally. -- actor-policy (REASONED; github-auth, datasette-docs): Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it. -- root-url (REASONED; datasette-docs): datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential. -- canned-writes (REASONED; datasette-docs): Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode. -- write-plugin (REASONED; write, datasette-docs): datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes. -- sql-policy (REASONED; datasette-docs): ?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists. -- query-limits (REASONED; datasette-docs): Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service. -- downloads (DEMONSTRATED; datasette-docs): allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403. -- cors (DEMONSTRATED; datasette-docs): The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag. -- introspection (REASONED; datasette-docs): /-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission. -- hash-urls (REASONED; datasette-docs): Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization. -- csrf (REASONED; datasette-docs): Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads. -- plugin-secrets (DEMONSTRATED; passwords, datasette-docs): The local analyst saw inline password hashes in metadata, but only $env references after environment indirection. -- web-bind (DEMONSTRATED; web, web-source): Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory. -- web-capabilities (REASONED; web): sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise. -- web-container (REASONED; web-image, web): sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy. -- web-password (DEMONSTRATED; web, web-source): Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/. -- web-readonly (DEMONSTRATED; web, web-source, sqlite): In the local --read-only run the anonymous read still returned 200 while the insert changed nothing. -- web-methods (REASONED; web-source): Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access. -- web-tls (REASONED; web): sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy. -- web-csrf (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy. -- web-cookie (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults. -- fronting (REASONED; nginx, caddy-docs, datasette-docs): Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route. -- identity-proxy (REASONED; iap, access): IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes. -- mfa (REASONED; web, access, datasette-docs): Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA. -- tailnet (REASONED; tailscale): Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured. -- origin-bypass (REASONED; iap): Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions. -- os-account (REASONED; sqlite): Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority. -- verify-inventory (REASONED; web, ss, docker, cloudrun, datasette-docs): Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability. -- verify-external (REASONED; web, iap, curl, datasette-docs): External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available. -- verify-anonymous (REASONED; web, datasette-docs): UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control. -- datasette-exposed (DEMONSTRATED; datasette-docs): Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200. -- web-exposed (DEMONSTRATED; web, web-source): Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database. -- verify-authorized (REASONED; web, curl, iap, access, datasette-docs): Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy. -- actor-results (DEMONSTRATED; passwords, datasette-docs): Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false. -- web-auth-results (DEMONSTRATED; web, web-source): Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/. -- introspection-results (DEMONSTRATED; passwords, datasette-docs): Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst. -- caddy-results (DEMONSTRATED; caddy-docs, datasette-docs): Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation. -- local-checks (DEMONSTRATED; curl): Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes. -- guard-limits (DEMONSTRATED; curl): Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage. -- datasette-cli (REASONED; datasette-docs): Bare datasette invokes serve; -h/--host and -p/--port override its bind and port. -- web-cli (REASONED; web, web-source): sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080. -- write-test (DEMONSTRATED; write, datasette-docs): Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row. -- secret-indirection (REASONED; datasette-docs): Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files. -- web-auth-config (REASONED; web, web-source, web-tag-source): In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles. -- web-readonly-config (REASONED; web, web-source, sqlite): --read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers. -- cors-policy (REASONED; datasette-docs): --cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests. -- query-pages (REASONED; datasette-docs): Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary. - - -An SQLite HTTP front-end turns a local file into a network service: **the port is the exposure**. Datasette allows anonymous browsing and read-only SQL by default; sqlite-web provides queries, imports, exports, row changes, and schema deletion. An unprotected, reachable instance exposes those capabilities. See Datasette permissions and sqlite-web features. - -A reachable, unauthenticated, writable sqlite-web instance therefore permits compromise of the databases its process can modify. That is an inference from its documented capabilities, not a claim of automatic host compromise. +A reachable, unauthenticated, writable sqlite-web instance therefore permits compromise of the databases its process can modify. That is an inference from its documented capabilities, not a claim of automatic host compromise. Use [sqlite.md](sqlite.md) for the files at rest and [web-exposure.md](web-exposure.md) for accidental file publication. Application-level SQL injection is outside this guide's scope; see the OWASP SQL Injection Prevention Cheat Sheet. @@ -604,3 +278,355 @@ Every URL listed below was fetched during authoring, except the sqlite-web 0.8.1 - curl manual (write-out variables `exitcode` and `errormsg` added in curl 7.75.0): https://curl.se/docs/manpage.html - ss manual: https://man7.org/linux/man-pages/man8/ss.8.html - OWASP SQL injection prevention: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.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/sqlite-http-frontends.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): + +- ds-container: Datasette container generator (caf238aac86ebe370959b25384d6e05f6a1e2359) +- web: sqlite-web package (0.8.1) +- web-tag-source: sqlite-web tagged implementation (0.8.1) +- ds-cloud: Datasette Cloud Run publisher (e889403d3bbe143854262682161c98a57bdb6594) +- fly: Datasette Fly publishing plugin (unknown) +- datasette-docs: Datasette documentation (rolling) (unknown) +- web-source: sqlite-web implementation (825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2) +- web-image: sqlite-web Dockerfile (7f6658b012a789f0b9418cad543e7798ce850130) +- passwords: datasette-auth-passwords (1.1.1) +- github-auth: datasette-auth-github (unknown) +- write: datasette-write (0.4) +- sqlite: SQLite documentation (unknown) +- docker: Docker localhost-publication boundary (28.0.0) +- cloudrun: Cloud Run documentation (unknown) +- nginx: nginx documentation (unknown) +- iap: Google IAP documentation (unknown) +- access: Cloudflare Access documentation (unknown) +- tailscale: Tailscale documentation (unknown) +- caddy-docs: Caddy documentation (rolling) (unknown) +- curl: curl minimum write-out version (7.75.0) +- ss: ss documentation (unknown) + +Claims (full source mapping in the JSON block below): + +- release (REASONED; datasette-docs): The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes. +- datasette-bind (DEMONSTRATED; datasette-docs): Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory. +- immutable (REASONED; sqlite, datasette-docs): Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors. +- datasette-docker (REASONED; datasette-docs): Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate. +- container-generator (REASONED; ds-container): At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address. +- cloudrun (REASONED; ds-cloud, cloudrun): Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization. +- publish-plugins (REASONED; fly, datasette-docs): Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established. +- docker-publication (REASONED; docker): Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally. +- actor-policy (REASONED; github-auth, datasette-docs): Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it. +- root-url (REASONED; datasette-docs): datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential. +- canned-writes (REASONED; datasette-docs): Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode. +- write-plugin (REASONED; write, datasette-docs): datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes. +- sql-policy (REASONED; datasette-docs): ?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists. +- query-limits (REASONED; datasette-docs): Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service. +- downloads (DEMONSTRATED; datasette-docs): allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403. +- cors (DEMONSTRATED; datasette-docs): The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag. +- introspection (REASONED; datasette-docs): /-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission. +- hash-urls (REASONED; datasette-docs): Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization. +- csrf (REASONED; datasette-docs): Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads. +- plugin-secrets (DEMONSTRATED; passwords, datasette-docs): The local analyst saw inline password hashes in metadata, but only $env references after environment indirection. +- web-bind (DEMONSTRATED; web, web-source): Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory. +- web-capabilities (REASONED; web): sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise. +- web-container (REASONED; web-image, web): sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy. +- web-password (DEMONSTRATED; web, web-source): Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/. +- web-readonly (DEMONSTRATED; web, web-source, sqlite): In the local --read-only run the anonymous read still returned 200 while the insert changed nothing. +- web-methods (REASONED; web-source): Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access. +- web-tls (REASONED; web): sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy. +- web-csrf (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy. +- web-cookie (DEMONSTRATED; web, web-source): sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults. +- fronting (REASONED; nginx, caddy-docs, datasette-docs): Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route. +- identity-proxy (REASONED; iap, access): IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes. +- mfa (REASONED; web, access, datasette-docs): Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA. +- tailnet (REASONED; tailscale): Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured. +- origin-bypass (REASONED; iap): Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions. +- os-account (REASONED; sqlite): Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority. +- verify-inventory (REASONED; web, ss, docker, cloudrun, datasette-docs): Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability. +- verify-external (REASONED; web, iap, curl, datasette-docs): External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available. +- verify-anonymous (REASONED; web, datasette-docs): UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control. +- datasette-exposed (DEMONSTRATED; datasette-docs): Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200. +- web-exposed (DEMONSTRATED; web, web-source): Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database. +- verify-authorized (REASONED; web, curl, iap, access, datasette-docs): Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy. +- actor-results (DEMONSTRATED; passwords, datasette-docs): Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false. +- web-auth-results (DEMONSTRATED; web, web-source): Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/. +- introspection-results (DEMONSTRATED; passwords, datasette-docs): Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst. +- caddy-results (DEMONSTRATED; caddy-docs, datasette-docs): Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation. +- local-checks (DEMONSTRATED; curl): Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes. +- guard-limits (DEMONSTRATED; curl): Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage. +- datasette-cli (REASONED; datasette-docs): Bare datasette invokes serve; -h/--host and -p/--port override its bind and port. +- web-cli (REASONED; web, web-source): sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080. +- write-test (DEMONSTRATED; write, datasette-docs): Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row. +- secret-indirection (REASONED; datasette-docs): Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files. +- web-auth-config (REASONED; web, web-source, web-tag-source): In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles. +- web-readonly-config (REASONED; web, web-source, sqlite): --read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers. +- cors-policy (REASONED; datasette-docs): --cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests. +- query-pages (REASONED; datasette-docs): Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary. + +````json +{ + "schema": 1, + "checked": "2026-09-27", + "documentation_checked": "2026-09", + "body_sha256": "cd2fba66ff332b791a1934a25ece6c5ef7f76fc791a8a4019819189f360eab0b", + "components": { + "ds-container": { + "name": "Datasette container generator", + "basis": "caf238aac86ebe370959b25384d6e05f6a1e2359", + "sources": { + "s3c9be24424e0": "https://raw.githubusercontent.com/simonw/datasette/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py", + "sffebbc2c5528": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L576-L655", + "seae12396d87a": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/utils/__init__.py#L658-L736", + "s9e735f75bf4b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L173-L179", + "s224af1264bb1": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L203-L306", + "sf0ea47721a46": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/cli.py#L352-L358", + "sae5997e91b6b": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/cloudrun.py#L17-L244", + "s735b0ff17b61": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/datasette/publish/heroku.py#L21-L254", + "s2484732fbf31": "https://github.com/simonw/datasette/blob/caf238aac86ebe370959b25384d6e05f6a1e2359/tests/test_package.py#L11-L61" + } + }, + "web": { + "name": "sqlite-web package", + "basis": "0.8.1", + "sources": { + "s91f8591a5f84": "https://github.com/coleifer/sqlite-web/blob/0.8.1/README.md" + } + }, + "web-tag-source": { + "name": "sqlite-web tagged implementation", + "basis": "0.8.1", + "sources": { + "s1626cd0d975f": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L3", + "s8c12f9aab57c": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1735-L1740", + "s30bba3ce63c6": "https://github.com/coleifer/sqlite-web/blob/0.8.1/sqlite_web/sqlite_web.py#L1944-L1956" + } + }, + "ds-cloud": { + "name": "Datasette Cloud Run publisher", + "basis": "e889403d3bbe143854262682161c98a57bdb6594", + "sources": { + "sf5859249ea9f": "https://github.com/simonw/datasette/blob/e889403d3bbe143854262682161c98a57bdb6594/datasette/publish/cloudrun.py" + } + }, + "fly": { + "name": "Datasette Fly publishing plugin", + "basis": "unknown", + "sources": { + "s2a2db4192225": "https://github.com/simonw/datasette-publish-fly" + } + }, + "datasette-docs": { + "name": "Datasette documentation (rolling)", + "basis": "unknown", + "sources": { + "s3e64933fe2e9": "https://docs.datasette.io/en/stable/changelog.html", + "sb0aae2446988": "https://docs.datasette.io/en/stable/cli-reference.html", + "sca59b58c5c84": "https://docs.datasette.io/en/stable/performance.html", + "s8f2fe7a13b3f": "https://docs.datasette.io/en/stable/installation.html", + "se7cc29f79be8": "https://docs.datasette.io/en/stable/publish.html", + "s878851fd8eac": "https://docs.datasette.io/en/stable/authentication.html", + "s824eb176fda7": "https://docs.datasette.io/en/stable/sql_queries.html", + "s406cc998a5da": "https://docs.datasette.io/en/stable/internals.html#csrf-protection", + "se154b0fa47dd": "https://docs.datasette.io/en/stable/settings.html", + "s96c8e6562139": "https://docs.datasette.io/en/stable/json_api.html", + "s1e93b280c5af": "https://docs.datasette.io/en/stable/introspection.html", + "sb1b30f98f026": "https://docs.datasette.io/en/stable/plugins.html#secret-configuration-values", + "s53e84082f6d1": "https://docs.datasette.io/en/stable/deploying.html" + } + }, + "web-source": { + "name": "sqlite-web implementation", + "basis": "825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2", + "sources": { + "s13d71c0a8cb2": "https://raw.githubusercontent.com/coleifer/sqlite-web/825b4a050fe2a04e6c6cf4b8ee1f56eaea4feef2/sqlite_web/sqlite_web.py" + } + }, + "web-image": { + "name": "sqlite-web Dockerfile", + "basis": "7f6658b012a789f0b9418cad543e7798ce850130", + "sources": { + "s4c81e6becc20": "https://raw.githubusercontent.com/coleifer/sqlite-web/7f6658b012a789f0b9418cad543e7798ce850130/docker/Dockerfile" + } + }, + "passwords": { + "name": "datasette-auth-passwords", + "basis": "1.1.1", + "sources": { + "sb6597be5af67": "https://github.com/simonw/datasette-auth-passwords/blob/1.1.1/README.md" + } + }, + "github-auth": { + "name": "datasette-auth-github", + "basis": "unknown", + "sources": { + "s90107b903580": "https://github.com/simonw/datasette-auth-github" + } + }, + "write": { + "name": "datasette-write", + "basis": "0.4", + "sources": { + "s073e10d21091": "https://github.com/simonw/datasette-write/blob/0.4/README.md" + } + }, + "sqlite": { + "name": "SQLite documentation", + "basis": "unknown", + "sources": { + "s4e4e320abf5b": "https://www.sqlite.org/uri.html", + "s2721da91f8d2": "https://www.sqlite.org/serverless.html", + "sadf0431c1271": "https://www.sqlite.org/security.html" + } + }, + "docker": { + "name": "Docker localhost-publication boundary", + "basis": "28.0.0", + "sources": { + "s1e53417c513d": "https://docs.docker.com/engine/network/port-publishing/" + } + }, + "cloudrun": { + "name": "Cloud Run documentation", + "basis": "unknown", + "sources": { + "sb90a6c13388a": "https://docs.cloud.google.com/run/docs/container-contract" + } + }, + "nginx": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s6a0728ca7609": "https://nginx.org/en/docs/http/configuring_https_servers.html", + "sf32ce2956917": "https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html" + } + }, + "iap": { + "name": "Google IAP documentation", + "basis": "unknown", + "sources": { + "saafdfc163fce": "https://docs.cloud.google.com/iap/docs/concepts-overview" + } + }, + "access": { + "name": "Cloudflare Access documentation", + "basis": "unknown", + "sources": { + "sfe56578bedf4": "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/", + "s06747cf7388c": "https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/independent-mfa/" + } + }, + "tailscale": { + "name": "Tailscale documentation", + "basis": "unknown", + "sources": { + "s5b9a9b15ae80": "https://tailscale.com/docs/reference/tailscale-cli/serve" + } + }, + "caddy-docs": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "sbc2bd9599bd1": "https://caddyserver.com/docs/caddyfile/directives/basic_auth" + } + }, + "curl": { + "name": "curl minimum write-out version", + "basis": "7.75.0", + "sources": { + "s2b2686afaf41": "https://curl.se/docs/manpage.html" + } + }, + "ss": { + "name": "ss documentation", + "basis": "unknown", + "sources": { + "sfc58f455acfa": "https://man7.org/linux/man-pages/man8/ss.8.html" + } + } + }, + "claims": { + "release": {"text": "The recorded stable Datasette documentation lists 0.65.5 with a table-permission bypass fix; deploy a supported release containing the security fixes.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9"], "status": "REASONED"}, + "datasette-bind": {"text": "Bare datasette serve without host or port flags listened on 127.0.0.1:8001 in the recorded unprivileged local ss inventory.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "DEMONSTRATED", "evidence": "bare `datasette serve` with no host or port flag listened on `127.0.0.1:8001`"}, + "immutable": {"text": "Use --immutable only for an unchanged snapshot; SQLite skips locking/change detection, so changing it can yield incorrect results or corruption errors.", "components": ["sqlite", "datasette-docs"], "sources": ["datasette-docs:sca59b58c5c84", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, + "datasette-docker": {"text": "Documented Docker launch binds 0.0.0.0:8001 and publishes 8001:8001; container bind and host publication are separate.", "components": ["datasette-docs"], "sources": ["datasette-docs:s8f2fe7a13b3f"], "status": "REASONED"}, + "container-generator": {"text": "At the pinned commit, make_dockerfile starts datasette serve --host 0.0.0.0, adds -i per supplied file (immutable mode), always adds --cors and --inspect-file inspect-data.json, conditionally adds --metadata, --template-dir templates/, --plugins-dir plugins/ and --version-note for truthy inputs, --static mount:mount per supplied mount, and whitespace-split truthy extra_options. It quotes those tokens with shlex.quote, then appends literal unquoted --port $PORT in shell-form CMD; port defaults to 8001 for ENV PORT and EXPOSE. Runtime expansion is not demonstrated. Package and publish cloudrun use the wrapper; Heroku has a separate Procfile path, and other publishers are not established. This guide advises reviewing the generated command, not just its bind address.", "components": ["ds-container"], "sources": ["ds-container:s3c9be24424e0", "ds-container:sffebbc2c5528", "ds-container:seae12396d87a", "ds-container:s9e735f75bf4b", "ds-container:s224af1264bb1", "ds-container:sf0ea47721a46", "ds-container:sae5997e91b6b", "ds-container:s735b0ff17b61", "ds-container:s2484732fbf31"], "status": "REASONED"}, + "cloudrun": {"text": "Inspected publisher uses --allow-unauthenticated; Cloud Run requires 0.0.0.0 and PORT, ingress default 8080, with external TLS termination but no implied application authorization.", "components": ["ds-cloud", "cloudrun"], "sources": ["ds-cloud:sf5859249ea9f", "cloudrun:sb90a6c13388a"], "status": "REASONED"}, + "publish-plugins": {"text": "Review each publishing plugin and provider hostname; datasette-publish-fly documents app.fly.dev and no universal plugin authentication default was established.", "components": ["fly", "datasette-docs"], "sources": ["datasette-docs:se7cc29f79be8", "fly:s2a2db4192225"], "status": "REASONED"}, + "docker-publication": {"text": "Address-less Docker publication binds all host addresses; use explicit host loopback mappings for 8001 or 8080, and check direct routing and the pre-28.0.0 caveat externally.", "components": ["docker"], "sources": ["docker:s1e53417c513d"], "status": "REASONED"}, + "actor-policy": {"text": "Metadata allow blocks authorize actors at instance, database, table and canned-query scope; authentication alone does not restrict access. GitHub authentication explicitly leaves anonymous access until permissions restrict it.", "components": ["github-auth", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "github-auth:s90107b903580"], "status": "REASONED"}, + "root-url": {"text": "datasette --root issues a one-use root-cookie login URL, not an anonymous-access restriction; treat the URL as a credential.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:sb0aae2446988"], "status": "REASONED"}, + "canned-writes": {"text": "Ordinary SQL is read-only; canned queries enable writes with write:true and plugins may add further mutable routes, not a global write mode.", "components": ["datasette-docs"], "sources": ["datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "write-plugin": {"text": "datasette-write adds /db/-/write governed by its own permission, root-only by default; installing it does not grant anonymous writes.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "REASONED"}, + "sql-policy": {"text": "?sql= and .json?sql= allow arbitrary reads; table/view allow rules do not prevent SQL reading those objects. Set allow_sql policy or default_allow_sql off; no generic --setting permissions exists.", "components": ["datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "query-limits": {"text": "Queries default to a one-second time limit; repeated expensive requests may still consume resources, an inference rather than demonstrated denial of service.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"}, + "downloads": {"text": "allow_download defaults on for immutable file-backed databases; disabling it controls whole-file downloads, not ordinary exports. Local /published.db changed from 200 to 403.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd"], "status": "DEMONSTRATED", "evidence": "`/published.db` returned `403`; without `--setting allow_download off` it returned `200`."}, + "cors": {"text": "The local --cors run added Access-Control-Allow-Origin: *; the header was absent without the flag.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "DEMONSTRATED", "evidence": "`--cors` added `Access-Control-Allow-Origin: *`, absent without it."}, + "introspection": {"text": "/-/metadata, /-/plugins, /-/versions, /-/settings and /-/databases, including JSON forms, expose instance details; permissions debugger separately requires root or its permission.", "components": ["datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, + "hash-urls": {"text": "Core hash_urls was removed in 0.61; datasette-hashed-urls provides cache-oriented content hashes, not secrecy or authorization.", "components": ["datasette-docs"], "sources": ["datasette-docs:s3e64933fe2e9", "datasette-docs:sca59b58c5c84"], "status": "REASONED"}, + "csrf": {"text": "Datasette asgi-csrf compares ds_csrftoken cookie with csrftoken form field or x-csrftoken header; preserve it for writes and review skip_csrf plugins. It does not restrict anonymous reads.", "components": ["datasette-docs"], "sources": ["datasette-docs:s406cc998a5da"], "status": "REASONED"}, + "plugin-secrets": {"text": "The local analyst saw inline password hashes in metadata, but only $env references after environment indirection.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "`/-/metadata.json` showed that analyst both users' password hashes when they were written inline in the plugin configuration, but only the `$env` references when they were supplied through environment variables."}, + "web-bind": {"text": "Bare sqlite_web listened on 127.0.0.1:8080 in the recorded unprivileged local ss inventory.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "bare `sqlite_web` on `127.0.0.1:8080`, as `ss -tlnp` run without `sudo` by the same account showed"}, + "web-capabilities": {"text": "sqlite-web supports queries, imports/exports, row changes and dropping schema objects; reachable unauthenticated write access implies database compromise, not automatic host compromise.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, + "web-container": {"text": "sqlite-web Dockerfile starts -H 0.0.0.0 and exposes 8080; README publishes 8080:8080. Use explicit loopback publication for a same-host proxy.", "components": ["web-image", "web"], "sources": ["web-image:s4c81e6becc20", "web:s91f8591a5f84"], "status": "REASONED"}, + "web-password": {"text": "Local SQLITE_WEB_PASSWORD without -P left anonymous query access at 200; with -P the anonymous query redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "`SQLITE_WEB_PASSWORD` set without `-P` left sqlite-web open (`200` anonymously), as the source reading above says; with `-P` the anonymous query GET got `302` to `/login/`."}, + "web-readonly": {"text": "In the local --read-only run the anonymous read still returned 200 while the insert changed nothing.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "DEMONSTRATED", "evidence": "with `--read-only` the read still returned `200` and the insert changed nothing."}, + "web-methods": {"text": "Inspected sqlite-web query handler permits read GETs; writes, scripts and drops require POST. A harmless query GET establishes anonymous read access.", "components": ["web-source"], "sources": ["web-source:s13d71c0a8cb2"], "status": "REASONED"}, + "web-tls": {"text": "sqlite-web documents native --ssl-cert and --ssl-key; this deployment baseline uses the shared TLS proxy.", "components": ["web"], "sources": ["web:s91f8591a5f84"], "status": "REASONED"}, + "web-csrf": {"text": "sqlite-web 0.8.1 loopback insert accepted an authenticated POST without a token and with foreign Origin; the form has no CSRF field. Prefer read-only behind the proxy.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "an authenticated `POST` insert with no token and a foreign `Origin` header wrote a row, and the insert form contains no CSRF field."}, + "web-cookie": {"text": "sqlite-web 0.8.1 set HttpOnly; Path=/ without SameSite or Secure even over TLS; cross-site cookie sending therefore depends on browser defaults.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Its session cookie was set with `HttpOnly; Path=/` and no `SameSite` or `Secure` attribute, even over TLS"}, + "fronting": {"text": "Keep same-host origins on loopback and apply TLS and authentication to every UI, SQL, JSON, export, download and auxiliary route.", "components": ["nginx", "caddy-docs", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "nginx:s6a0728ca7609", "nginx:sf32ce2956917", "caddy-docs:sbc2bd9599bd1"], "status": "REASONED"}, + "identity-proxy": {"text": "IAP and Cloudflare Access evaluate access before forwarding; no tenancy was available to demonstrate these routes.", "components": ["iap", "access"], "sources": ["iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED"}, + "mfa": {"text": "Neither reviewed core authentication interface supplies native MFA; enforce it at the identity provider or through Cloudflare Access independent MFA.", "components": ["web", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "access:s06747cf7388c"], "status": "REASONED"}, + "tailnet": {"text": "Tailscale Serve shares inside the tailnet; Funnel publishes publicly. Verify which path is configured.", "components": ["tailscale"], "sources": ["tailscale:s5b9a9b15ae80"], "status": "REASONED"}, + "origin-bypass": {"text": "Inventory direct origins and provider hostnames so proxy login cannot be bypassed; IAP documents backend and Cloud Run hostname precautions.", "components": ["iap"], "sources": ["iap:saafdfc163fce"], "status": "REASONED"}, + "os-account": {"text": "Use a dedicated unprivileged account and curated snapshot whose file and parent it cannot modify; protect sidecars and temporary files under SQLite filesystem authority.", "components": ["sqlite"], "sources": ["sqlite:s2721da91f8d2", "sqlite:sadf0431c1271"], "status": "REASONED"}, + "verify-inventory": {"text": "Inspect complete listener table, IPv6 and unexpected ports; empty output after an error is inconclusive. Root ss, wildcard binds, Docker publication and managed ingress remain reasoned without sudo/container capability.", "components": ["web", "ss", "docker", "cloudrun", "datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "web:s91f8591a5f84", "ss:sfc58f455acfa", "docker:s1e53417c513d", "cloudrun:sb90a6c13388a"], "status": "REASONED", "verify": [1]}, + "verify-external": {"text": "External origin/provider tests require a successful local control and actual deployment targets; any HTTP response proves reachability, while transport failures alone prove no isolation. No second network/provider deployment was available.", "components": ["web", "iap", "curl", "datasette-docs"], "sources": ["datasette-docs:s53e84082f6d1", "web:s91f8591a5f84", "iap:saafdfc163fce", "curl:s2b2686afaf41"], "status": "REASONED", "verify": [2]}, + "verify-anonymous": {"text": "UI, schema or row data without credentials is exposure; login pages, unexplained 404s and HTTP success alone do not establish protection. SQL-disabled tests also need an authorized browsing control.", "components": ["web", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "datasette-docs:s824eb176fda7", "web:s91f8591a5f84"], "status": "REASONED", "verify": [2]}, + "datasette-exposed": {"text": "Local bare Datasette returned listing and known SQL row with 200 and /-/versions.json anonymously with 200; baseline SQL returned 403 while table JSON still returned 200.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988", "datasette-docs:se154b0fa47dd", "datasette-docs:s96c8e6562139", "datasette-docs:s1e93b280c5af"], "status": "DEMONSTRATED", "evidence": "Against the baseline command above, the SQL JSON route returned `403` while the table's JSON page still returned `200` (SQL disabled, browsing permitted)"}, + "web-exposed": {"text": "Local bare sqlite-web returned query GET 200 anonymously and accepted an anonymous insert into a disposable database.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "Bare `sqlite_web` answered the query GET with `200` anonymously, and an anonymous `POST` insert wrote a row to a disposable database"}, + "verify-authorized": {"text": "Private mode-0600 header file supplies credentials by path, not argv values; compare known authorized data with anonymous denial and repeat with an unauthorized actor. File guards establish readability/type, not privacy; identity-proxy scenarios lack a tenancy.", "components": ["web", "curl", "iap", "access", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "web:s91f8591a5f84", "curl:s2b2686afaf41", "iap:saafdfc163fce", "access:sfe56578bedf4"], "status": "REASONED", "verify": [3]}, + "actor-results": {"text": "Local password-plugin analyst cookie returned table rows with 200, anonymous and guest requests got 403, and analyst SQL got 403 under allow_sql:false.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "the block returned the table's rows to the `analyst` session cookie (`authorized http=200`) and `403` anonymously; an authenticated `guest` got `403`, the unauthorized outcome, and the analyst got `403` on SQL under `allow_sql: false`."}, + "web-auth-results": {"text": "Local sqlite-web -P session cookie returned the row with 200 and the anonymous request redirected to /login/.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "DEMONSTRATED", "evidence": "For sqlite-web with `-P`, the session cookie got the row with `200` and the anonymous request `302` to `/login/`."}, + "introspection-results": {"text": "Under the local metadata policy, all five auxiliary JSON routes returned 403 anonymously and 200 to a freshly logged-in analyst.", "components": ["passwords", "datasette-docs"], "sources": ["datasette-docs:s1e93b280c5af", "datasette-docs:s878851fd8eac", "passwords:sb6597be5af67"], "status": "DEMONSTRATED", "evidence": "Under the same metadata, `/-/versions.json`, `/-/metadata.json`, `/-/plugins.json`, `/-/settings.json` and `/-/databases.json` returned `403` anonymously and `200` to a freshly logged-in analyst"}, + "caddy-results": {"text": "Caddy v2.11.4 loopback TLS/Basic proxy denied anonymous UI, SQL, database download, versions and CSV with 401 and allowed credentials with 200; direct backend remained anonymous. Protection depends on backend isolation.", "components": ["caddy-docs", "datasette-docs"], "sources": ["caddy-docs:sbc2bd9599bd1", "datasette-docs:s53e84082f6d1"], "status": "DEMONSTRATED", "evidence": "Block C through the proxy gave `authorized http=200` with the rows and `anonymous http=401` with `www-authenticate: Basic realm=\"restricted\"`."}, + "local-checks": {"text": "Recorded bash parsing, metadata JSON parsing and 120 guard executions passed; curl argument recording tested guards separately from service outcomes.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Local authoring checks passed: `bash -n` on all five shell blocks, parsing of the metadata JSON, and 120 guard executions covering 40 cases under ordinary Bash, `set -u`, and `set -u` with `IFS=0`."}, + "guard-limits": {"text": "Use Bash with real unshadowed builtins and curl 7.75.0 or newer; paste whole blocks, escape apostrophes, avoid credential URLs, and retain TLS verification. Partial pastes and inherited matching marker/arguments remain outside guard coverage.", "components": ["curl"], "sources": ["curl:s2b2686afaf41"], "status": "DEMONSTRATED", "evidence": "Across 48 block B/C cases under ordinary Bash, set -u, and set -u with IFS=0, an explicitly substituted curl argument recorder ran with valid whole-block values, inherited matching marker/arguments, and pastes below the guards. Whole-block placeholders and omitted assignments without a matching marker exited 2 without calling it."}, + "datasette-cli": {"text": "Bare datasette invokes serve; -h/--host and -p/--port override its bind and port.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb0aae2446988"], "status": "REASONED"}, + "web-cli": {"text": "sqlite-web -H/--host uses uppercase H and -p/--port overrides the port; the documented default is 127.0.0.1:8080.", "components": ["web", "web-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2"], "status": "REASONED"}, + "write-test": {"text": "Local anonymous write and root write without CSRF were denied; root write with the token returned 302 and wrote the row.", "components": ["write", "datasette-docs"], "sources": ["datasette-docs:s406cc998a5da", "write:s073e10d21091"], "status": "DEMONSTRATED", "evidence": "With datasette-write on a disposable database, an anonymous write got `403`, a root write without the CSRF token `403`, and with the token `302`, and the row was written."}, + "secret-indirection": {"text": "Keep plugin secrets out of public metadata with documented environment or file indirection, and protect root login URLs and credential files.", "components": ["datasette-docs"], "sources": ["datasette-docs:sb1b30f98f026", "datasette-docs:s878851fd8eac"], "status": "REASONED"}, + "web-auth-config": {"text": "In tagged sqlite-web 0.8.1, a nonempty shared password requires -P/--password; SQLITE_WEB_PASSWORD is read only inside that option branch, and an unset or empty variable leads to a prompt. The shared login supplies no per-table roles.", "components": ["web", "web-source", "web-tag-source"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "web-tag-source:s1626cd0d975f", "web-tag-source:s8c12f9aab57c", "web-tag-source:s30bba3ce63c6"], "status": "REASONED"}, + "web-readonly-config": {"text": "--read-only opens SQLite with mode=ro and reduces exposure to reads; it does not authenticate callers.", "components": ["web", "web-source", "sqlite"], "sources": ["web:s91f8591a5f84", "web-source:s13d71c0a8cb2", "sqlite:s4e4e320abf5b"], "status": "REASONED"}, + "cors-policy": {"text": "--cors permits cross-origin JSON access and should be omitted unless intended; its absence does not authenticate requests.", "components": ["datasette-docs"], "sources": ["datasette-docs:s96c8e6562139"], "status": "REASONED"}, + "query-pages": {"text": "Queries return at most 1,000 rows at a time by default; pagination retrieves more, so that limit is not a confidentiality boundary.", "components": ["datasette-docs"], "sources": ["datasette-docs:se154b0fa47dd", "datasette-docs:s824eb176fda7"], "status": "REASONED"} + } +} +```` + diff --git a/sqlite.md b/sqlite.md index ed5228c44..dae404d00 100644 --- a/sqlite.md +++ b/sqlite.md @@ -1,341 +1,8 @@ ---- -version_basis: { - "schema": 1, - "checked": "2026-10-17", - "documentation_checked": "2026-09", - "body_sha256": "1df2b900bb4797e8f2561a2cb2066c6c74bf8505a9043279c660e1d96229198f", - "components": { - "see": { - "name": "SQLite Encryption Extension", - "basis": "unknown", - "sources": { - "sda3d8876503d": "https://www.sqlite.org/see/doc/trunk/www/readme.wiki" - } - }, - "cipher": { - "name": "SQLCipher", - "basis": "unknown", - "sources": { - "s57d83a42a74a": "https://www.zetetic.net/sqlcipher/" - } - }, - "turso": { - "name": "Turso documentation", - "basis": "unknown", - "sources": { - "sb32fd9c679e1": "https://docs.turso.tech/sdk/http/quickstart", - "sfec4e734f423": "https://docs.turso.tech/cli/db/tokens/create", - "se409838a474a": "https://docs.turso.tech/sdk/http/reference" - } - }, - "litestream": { - "name": "Litestream documentation", - "basis": "unknown", - "sources": { - "sec3d72af1447": "https://litestream.io/guides/", - "s87fa51accafc": "https://litestream.io/guides/s3/", - "sa5dddf04652c": "https://litestream.io/reference/config/" - } - }, - "mcp": { - "name": "Litestream MCP minimum", - "basis": "v0.5.0", - "sources": { - "sa5dddf04652c": "https://litestream.io/reference/config/" - } - }, - "litefs": { - "name": "LiteFS source", - "basis": "v0.5.14", - "sources": { - "s694a6ba8eafa": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L134-L268", - "s864cacb8e7cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L76-L99", - "s16da1761d6cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/client.go#L32-L43", - "sf0e982d0e36a": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L57", - "s9389d639da6b": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L54-L58", - "s475c28fd949e": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L76-L110", - "s055e15e84ba5": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L219-L233", - "s0b0bac964067": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L288-L333", - "s326a1007e5c2": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L294-L318", - "s4547cc32557d": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2779-L2811", - "s85ece0e83d43": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1904-L1910", - "sa467eeb47f34": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L320-L346", - "s5ffa5c20fc78": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2682-L2776", - "s02908966756d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L495-L520", - "sd63fa776fbc1": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L526-L585", - "s1c51b7efc42d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L686-L699", - "scd0168363c1a": "https://github.com/superfly/litefs/blob/v0.5.14/http/http.go#L15-L43", - "s298808a10d50": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L461-L493", - "s7269cca8b0ea": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2387-L2448", - "sbd59a1af784e": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2452-L2609", - "sa199174f541b": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L348-L407", - "s750403129022": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L215-L325", - "s5009045b97e1": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1525-L1531", - "se2add54cf935": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L39-L41", - "s59a2dfccd853": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1455-L1465", - "s246e6ad0d330": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L409-L459", - "sae4ece3fbc87": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L408-L432", - "s7bcb006d6884": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L168-L170", - "se1a41376a98d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L271-L292", - "s93c0ad91a567": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L779-L803", - "s36792457dd64": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L487-L488", - "s7234e34af9ef": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1661-L1713", - "s4cebe21e711d": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L329-L348", - "sb43f5d78723c": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L241-L244", - "s8dfa3f10f3d3": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L99-L131", - "s83d8bd834347": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1384-L1387", - "s3284d1a3ecad": "https://github.com/superfly/litefs/blob/v0.5.14/Dockerfile", - "s2b80fdf8adbe": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L519-L524", - "sc0568fe7e0ee": "https://github.com/superfly/litefs/blob/v0.5.14/http/proxy_server.go#L131-L144", - "sf0d31f900082": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L60-L68", - "s82103ee03d64": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L32-L35" - } - }, - "litefs-docs": { - "name": "LiteFS documentation", - "basis": "unknown", - "sources": { - "s13b9e92d9996": "https://fly.io/docs/litefs/", - "sd7c0ba136b25": "https://fly.io/docs/litefs/config/" - } - }, - "go": { - "name": "Go library documentation", - "basis": "unknown", - "sources": { - "s6c5be8454523": "https://pkg.go.dev/github.com/golang/net/http2/h2c#NewHandler", - "s37204ff1b27c": "https://pkg.go.dev/net#Listen", - "se49529035a0c": "https://pkg.go.dev/expvar", - "s0246034dddd1": "https://pkg.go.dev/net/http/pprof" - } - }, - "docker": { - "name": "Docker documentation", - "basis": "unknown", - "sources": { - "s881082f899b2": "https://docs.docker.com/engine/network/drivers/bridge/", - "s6732393fd414": "https://docs.docker.com/engine/network/drivers/host/", - "s00095bd02553": "https://docs.docker.com/engine/network/#container-networks" - } - }, - "sqlite-rolling": { - "name": "SQLite documentation (rolling)", - "basis": "unknown", - "sources": { - "s2721da91f8d2": "https://www.sqlite.org/serverless.html", - "sf3d7f8391994": "https://www.sqlite.org/tempfiles.html", - "s742deb3338bb": "https://sqlite.org/loadext.html", - "sadf0431c1271": "https://www.sqlite.org/security.html" - } - }, - "git": { - "name": "Git documentation", - "basis": "v2.56.0", - "sources": { - "s12aad6467212": "https://github.com/git/git/blob/v2.56.0/Documentation/gitignore.adoc#L15-L18", - "s8054ab09dc74": "https://github.com/git/git/blob/v2.56.0/Documentation/git-rm.adoc#L17-L28", - "s9a6802adec89": "https://github.com/git/git/blob/v2.56.0/Documentation/git-log.adoc#L16-L16" - } - }, - "coreutils": { - "name": "GNU Coreutils stat manual", - "basis": "v9.7", - "sources": { - "s247df4545dc0": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L12989-L12995", - "sfd49553ab6e5": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13046", - "sbd6f7cad7bf4": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13062", - "s3e55a7242c33": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13073" - } - }, - "bash": { - "name": "GNU Bash reference manual", - "basis": "bash-5.3", - "sources": { - "s3711969be580": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n450", - "s7fec9b923488": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n769", - "s77b19e384d70": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1397", - "se4f37e01a249": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1866", - "s07f77f16f9f5": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2288", - "sbed8cd562a35": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2993", - "s7c9e44003867": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3131", - "s223b5c4d9a28": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4350", - "se02ece9163d4": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4582", - "s6067b2b1f78a": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5216", - "sa506f1abd6a1": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6182", - "s7d29125b4005": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n8232", - "s8962e63cfb20": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2580", - "s8a27e686ddfb": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5466", - "s213f6d8d1f07": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3280", - "sf9757e9fd6e8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3872", - "s8cbc1b4709e2": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4737", - "s3255eca7e496": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4852", - "s252aa2e81179": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5156", - "s8447036b2114": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5161", - "s5cab49305f3d": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5548", - "s6e8ae812c1c3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5625", - "sb38bf74a89b0": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5629", - "s260459961fbc": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5659", - "s8c59508bd4c8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5917", - "s8016d1aecfca": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5929", - "se62fd3ad4fc3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6122", - "s2df39d2c9c1c": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6194", - "s80e3246e96d8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n7274" - } - }, - "curl": { - "name": "curl", - "basis": "curl-8_14_1", - "sources": { - "s5b71321b79ea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/disable.md#L4-L20", - "sc405a4bffc27": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/globoff.md#L4-L22", - "s345ec65025b0": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/noproxy.md#L18-L28", - "sc9973c4ceec1": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/connect-timeout.md#L19-L28", - "s7b6dca376156": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/max-time.md#L21-L27", - "s156481f6b726": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/silent.md#L4-L25", - "s07916104bf60": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/show-error.md#L4-L19", - "sa937dbe5dd4a": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L4-L33", - "s021603e35e68": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L49-L53", - "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", - "s39402f9cb25c": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L105-L107", - "sf2dcd83157a6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/insecure.md#L4-L30", - "s13f463154cea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/fail.md#L24-L28", - "s2f1ff2f6cacd": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/output.md#L4-L53", - "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49", - "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" - } - }, - "grep-docs": { - "name": "GNU grep documentation", - "basis": "v3.12", - "sources": { - "s1733b640052b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197", - "s99f121f5426a": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342", - "sbc8d05ab659b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621", - "sfdd7a73eab39": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757", - "s7f9dc35e81c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788", - "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", - "sc08d6af4d642": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142", - "scd528c854b46": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150", - "sb8be8b716426": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274", - "s5d5af9297df2": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316", - "se99a83dba3c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363" - } - } - }, - "claims": { - "filesystem": {"text": "No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sadf0431c1271"], "status": "REASONED"}, - "extensions": {"text": "Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s742deb3338bb"], "status": "REASONED"}, - "web-files": {"text": "Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "git-files": {"text": "Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit.", "components": ["sqlite-rolling", "git"], "sources": ["sqlite-rolling:sf3d7f8391994", "git:s12aad6467212", "git:s8054ab09dc74", "git:s9a6802adec89"], "status": "REASONED"}, - "permissions": {"text": "Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "temporary": {"text": "Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "encryption": {"text": "Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption.", "components": ["see", "cipher"], "sources": ["see:sda3d8876503d", "cipher:s57d83a42a74a"], "status": "REASONED"}, - "encryption-keys": {"text": "Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak.", "components": ["see", "sqlite-rolling"], "sources": ["see:sda3d8876503d", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, - "turso-http": {"text": "Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git.", "components": ["turso"], "sources": ["turso:sb32fd9c679e1"], "status": "REASONED"}, - "turso-scope": {"text": "db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens.", "components": ["turso"], "sources": ["turso:sfec4e734f423"], "status": "REASONED"}, - "replicas": {"text": "Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials.", "components": ["litestream"], "sources": ["litestream:sec3d72af1447", "litestream:s87fa51accafc"], "status": "REASONED"}, - "metrics": {"text": "Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090.", "components": ["litestream"], "sources": ["litestream:sa5dddf04652c"], "status": "REASONED"}, - "mcp": {"text": "Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001.", "components": ["mcp"], "sources": ["mcp:sa5dddf04652c"], "status": "REASONED"}, - "litefs-backups": {"text": "LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations.", "components": ["litefs-docs"], "sources": ["litefs-docs:s13b9e92d9996"], "status": "REASONED"}, - "litefs-transport": {"text": "LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc", "litefs:s16da1761d6cc", "go:s6c5be8454523"], "status": "REASONED"}, - "litefs-bind": {"text": "http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port.", "components": ["litefs", "go"], "sources": ["litefs:sf0e982d0e36a", "litefs:s9389d639da6b", "go:s37204ff1b27c", "litefs:s82103ee03d64"], "status": "REASONED"}, - "litefs-config": {"text": "Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion.", "components": ["litefs"], "sources": ["litefs:s475c28fd949e", "litefs:s055e15e84ba5", "litefs:s0b0bac964067"], "status": "REASONED"}, - "import": {"text": "POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status.", "components": ["litefs"], "sources": ["litefs:s326a1007e5c2", "litefs:s4547cc32557d", "litefs:s85ece0e83d43"], "status": "REASONED"}, - "export": {"text": "GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404.", "components": ["litefs"], "sources": ["litefs:sa467eeb47f34", "litefs:s5ffa5c20fc78"], "status": "REASONED"}, - "stream": {"text": "POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots.", "components": ["litefs"], "sources": ["litefs:s02908966756d", "litefs:sd63fa776fbc1", "litefs:s1c51b7efc42d", "litefs:scd0168363c1a"], "status": "REASONED"}, - "tx": {"text": "POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404.", "components": ["litefs"], "sources": ["litefs:s298808a10d50", "litefs:s7269cca8b0ea", "litefs:sbd59a1af784e"], "status": "REASONED"}, - "halt": {"text": "POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id.", "components": ["litefs"], "sources": ["litefs:sa199174f541b", "litefs:s750403129022", "litefs:s5009045b97e1"], "status": "REASONED"}, - "halt-ttl": {"text": "LiteFS halt-lock TTL defaults to 30 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"}, - "promote": {"text": "POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:sae4ece3fbc87"], "status": "REASONED"}, - "handoff": {"text": "POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:s7bcb006d6884"], "status": "REASONED"}, - "information": {"text": "GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:se1a41376a98d", "litefs:s93c0ad91a567"], "status": "REASONED"}, - "debug": {"text": "Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s36792457dd64", "litefs:s7234e34af9ef", "go:se49529035a0c", "go:s0246034dddd1"], "status": "REASONED"}, - "debug-rand": {"text": "/debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc"], "status": "REASONED"}, - "cluster-private": {"text": "Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy.", "components": ["litefs", "litefs-docs"], "sources": ["litefs:s694a6ba8eafa", "litefs-docs:sd7c0ba136b25"], "status": "REASONED"}, - "consul-url": {"text": "Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port.", "components": ["litefs"], "sources": ["litefs:s4cebe21e711d"], "status": "REASONED"}, - "static-url": {"text": "Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API.", "components": ["litefs"], "sources": ["litefs:s8dfa3f10f3d3", "litefs:s83d8bd834347", "litefs:sb43f5d78723c"], "status": "REASONED"}, - "container": {"text": "LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability.", "components": ["litefs", "docker"], "sources": ["litefs:s3284d1a3ecad", "docker:s881082f899b2", "docker:s6732393fd414", "docker:s00095bd02553"], "status": "REASONED"}, - "proxy": {"text": "LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary.", "components": ["litefs"], "sources": ["litefs:s2b80fdf8adbe", "litefs:sc0568fe7e0ee", "litefs:sf0d31f900082"], "status": "REASONED"}, - "verify-download": {"text": "Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available.", "components": ["sqlite-rolling", "bash", "curl"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [1]}, - "verify-bundle": {"text": "Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths.", "components": ["turso", "bash", "grep-docs"], "sources": ["turso:sb32fd9c679e1", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "grep-docs:s1733b640052b", "grep-docs:s99f121f5426a", "grep-docs:sbc8d05ab659b", "grep-docs:sfdd7a73eab39", "grep-docs:s7f9dc35e81c5", "grep-docs:s5170317ddea8", "grep-docs:sc08d6af4d642", "grep-docs:scd528c854b46", "grep-docs:sb8be8b716426", "grep-docs:s5d5af9297df2", "grep-docs:se99a83dba3c5"], "status": "DEMONSTRATED", "evidence": "A bundle carrying the placeholder printed `dist/app.js` before `token-literal exit: 0` and again before `jwt-shape exit: 0`, and a clean bundle printed `1` for both.", "verify": [2]}, - "verify-git": {"text": "Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "DEMONSTRATED", "evidence": "With `app.db` committed before an `app.db*` ignore rule was added, `git check-ignore -v` listed the sidecars but not the tracked `app.db`. `git ls-files` printed `app.db`, and `git log` printed its commit.", "verify": [3]}, - "verify-modes": {"text": "SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised.", "components": ["sqlite-rolling", "coreutils"], "sources": ["sqlite-rolling:sf3d7f8391994", "coreutils:s247df4545dc0", "coreutils:sfd49553ab6e5", "coreutils:sbd6f7cad7bf4", "coreutils:s3e55a7242c33"], "status": "REASONED", "verify": [4]}, - "verify-listeners": {"text": "Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available.", "components": ["litefs", "litestream", "mcp", "bash", "curl"], "sources": ["litefs:s694a6ba8eafa", "litestream:sa5dddf04652c", "mcp:sa5dddf04652c", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [5]}, - "verify-turso": {"text": "Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available.", "components": ["turso", "bash", "curl"], "sources": ["turso:se409838a474a", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:sd3b039bafbf6", "curl:s3db522258e44"], "status": "REASONED", "verify": [6]}, - "halt-expiration": {"text": "LiteFS checks halt-lock expiration every 5 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"} - } -} ---- # SQLite in deployment: the file is the exposure (plus Turso and Litestream) - -**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 SQLite documentation (rolling) (no recorded version), Litestream MCP minimum v0.5.0, LiteFS source v0.5.14, Git documentation v2.56.0, GNU Coreutils stat manual v9.7, GNU Bash reference manual bash-5.3, curl curl-8_14_1, and GNU grep documentation v3.12; 7 further sources carry no recorded version. Metadata reviewed 2026-10-17; documentation checked September 2026. The guide records 40 claims: 2 demonstrated by a check that ran and 38 reasoned from cited sources. -Components (recorded basis): - -- see: SQLite Encryption Extension (unknown) -- cipher: SQLCipher (unknown) -- turso: Turso documentation (unknown) -- litestream: Litestream documentation (unknown) -- mcp: Litestream MCP minimum (v0.5.0) -- litefs: LiteFS source (v0.5.14) -- litefs-docs: LiteFS documentation (unknown) -- go: Go library documentation (unknown) -- docker: Docker documentation (unknown) -- sqlite-rolling: SQLite documentation (rolling) (unknown) -- git: Git documentation (v2.56.0) -- coreutils: GNU Coreutils stat manual (v9.7) -- bash: GNU Bash reference manual (bash-5.3) -- curl: curl (curl-8_14_1) -- grep-docs: GNU grep documentation (v3.12) - -Claims (full source mapping in the front matter): - -- filesystem (REASONED; sqlite-rolling): No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect. -- extensions (REASONED; sqlite-rolling): Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries. -- web-files (REASONED; sqlite-rolling): Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names. -- git-files (REASONED; sqlite-rolling, git): Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit. -- permissions (REASONED; sqlite-rolling): Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation. -- temporary (REASONED; sqlite-rolling): Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions. -- encryption (REASONED; see, cipher): Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption. -- encryption-keys (REASONED; see, sqlite-rolling): Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak. -- turso-http (REASONED; turso): Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git. -- turso-scope (REASONED; turso): db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens. -- replicas (REASONED; litestream): Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials. -- metrics (REASONED; litestream): Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090. -- mcp (REASONED; mcp): Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001. -- litefs-backups (REASONED; litefs-docs): LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations. -- litefs-transport (REASONED; litefs, go): LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers. -- litefs-bind (REASONED; litefs, go): http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port. -- litefs-config (REASONED; litefs): Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion. -- import (REASONED; litefs): POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status. -- export (REASONED; litefs): GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404. -- stream (REASONED; litefs): POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots. -- tx (REASONED; litefs): POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404. -- halt (REASONED; litefs): POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id. -- halt-ttl (REASONED; litefs): LiteFS halt-lock TTL defaults to 30 seconds. -- promote (REASONED; litefs): POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required. -- handoff (REASONED; litefs): POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required. -- information (REASONED; litefs): GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database. -- debug (REASONED; litefs, go): Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name. -- debug-rand (REASONED; litefs): /debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client. -- cluster-private (REASONED; litefs, litefs-docs): Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy. -- consul-url (REASONED; litefs): Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port. -- static-url (REASONED; litefs): Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API. -- container (REASONED; litefs, docker): LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability. -- proxy (REASONED; litefs): LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary. -- verify-download (REASONED; sqlite-rolling, bash, curl): Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available. -- verify-bundle (DEMONSTRATED; turso, bash, grep-docs): Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths. -- verify-git (DEMONSTRATED; sqlite-rolling): Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded. -- verify-modes (REASONED; sqlite-rolling, coreutils): SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised. -- verify-listeners (REASONED; litefs, litestream, mcp, bash, curl): Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available. -- verify-turso (REASONED; turso, bash, curl): Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available. -- halt-expiration (REASONED; litefs): LiteFS checks halt-lock expiration every 5 seconds. - +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. SQLite has no server process and no network listener: "there are no other processes, threads, machines, or other mechanisms (apart from host computer OS and filesystem) to help provide database services" ([SQLite: serverless](https://www.sqlite.org/serverless.html)), so there is no database-side authentication and the only access control is the filesystem's. Its security page warns that "any database file which might have ever been writable by an agent in a different security domain should be treated as suspect." So the deployment exposure is rarely a flaw in SQLite itself; it is wherever the database file ends up: under a web root, inside a git repository, readable by another local account, or replicated to a public bucket. Run the application with access only to the files it needs, since `ATTACH` opens further databases under the process's filesystem authority; keep extension loading disabled unless required (core SQLite disables it by default, though the SQLite CLI enables it) and load only trusted extension libraries. Application-level SQL-injection prevention is outside this guide's scope. @@ -600,3 +267,362 @@ Applicability checked on 2026-09-18: SQLite 3.x documentation (local file tests - GNU grep documentation v3.12 (checked October 2026): [-f reads one pattern per line; - means stdin (197-207)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197); [-l prints matching file names and stops at the first match per file (342-351)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342); [default binary handling, suppression and matching limits (621-668)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621); [-r recurses, follows command-line symlinks and skips encountered symlinks (757-769)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757); [-- terminates options (788-794)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788); [exit 0 for a match, 1 for none, 2 for error, with the -q exception (1110-1115)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110). - GNU grep documentation v3.12 (checked October 2026): [-E selects extended regular expressions (1142-1148)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142); [-F selects fixed strings (1150-1156)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150); [ordinary regexp characters (1274-1277)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274); [{n,} repeats at least n times (1316-1320)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316); [bracket expressions match one listed character (1363-1373)](https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363). - curl curl-8_14_1 behaviour used by this guide's probes: the cited `-q`, `-g` and `--noproxy '*'` controls suppress curlrc, URL globbing and proxies; `--connect-timeout 5` bounds connection setup and `--max-time 20` bounds each transfer. The cited `-sS` combination hides progress while retaining errors, `-o /dev/null` suppresses bodies, and `-w` prints the response code, transfer exit code and error text even on failure. HTTP status is separate from transfer success. For Turso, the cited `--data` selects POST and `-H 'Content-Type: application/json'` replaces curl's default content type; the request payload and the required anonymous/authorized outcomes remain this guide's procedure using the Turso reference, not curl's guarantee. The cited default TLS checks support the requirement never to add `-k`. + + + +## 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/sqlite.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): + +- sqlite-rolling: SQLite documentation (rolling) (unknown) +- mcp: Litestream MCP minimum (v0.5.0) +- see: SQLite Encryption Extension (unknown) +- turso: Turso documentation (unknown) +- litestream: Litestream documentation (unknown) +- cipher: SQLCipher (unknown) +- litefs: LiteFS source (v0.5.14) +- litefs-docs: LiteFS documentation (unknown) +- go: Go library documentation (unknown) +- docker: Docker documentation (unknown) +- git: Git documentation (v2.56.0) +- coreutils: GNU Coreutils stat manual (v9.7) +- bash: GNU Bash reference manual (bash-5.3) +- curl: curl (curl-8_14_1) +- grep-docs: GNU grep documentation (v3.12) + +Claims (full source mapping in the JSON block below): + +- filesystem (REASONED; sqlite-rolling): No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect. +- extensions (REASONED; sqlite-rolling): Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries. +- web-files (REASONED; sqlite-rolling): Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names. +- git-files (REASONED; sqlite-rolling, git): Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit. +- permissions (REASONED; sqlite-rolling): Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation. +- temporary (REASONED; sqlite-rolling): Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions. +- encryption (REASONED; see, cipher): Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption. +- encryption-keys (REASONED; see, sqlite-rolling): Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak. +- turso-http (REASONED; turso): Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git. +- turso-scope (REASONED; turso): db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens. +- replicas (REASONED; litestream): Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials. +- metrics (REASONED; litestream): Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090. +- mcp (REASONED; mcp): Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001. +- litefs-backups (REASONED; litefs-docs): LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations. +- litefs-transport (REASONED; litefs, go): LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers. +- litefs-bind (REASONED; litefs, go): http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port. +- litefs-config (REASONED; litefs): Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion. +- import (REASONED; litefs): POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status. +- export (REASONED; litefs): GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404. +- stream (REASONED; litefs): POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots. +- tx (REASONED; litefs): POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404. +- halt (REASONED; litefs): POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id. +- halt-ttl (REASONED; litefs): LiteFS halt-lock TTL defaults to 30 seconds. +- promote (REASONED; litefs): POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required. +- handoff (REASONED; litefs): POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required. +- information (REASONED; litefs): GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database. +- debug (REASONED; litefs, go): Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name. +- debug-rand (REASONED; litefs): /debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client. +- cluster-private (REASONED; litefs, litefs-docs): Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy. +- consul-url (REASONED; litefs): Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port. +- static-url (REASONED; litefs): Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API. +- container (REASONED; litefs, docker): LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability. +- proxy (REASONED; litefs): LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary. +- verify-download (REASONED; sqlite-rolling, bash, curl): Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available. +- verify-bundle (DEMONSTRATED; turso, bash, grep-docs): Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths. +- verify-git (DEMONSTRATED; sqlite-rolling): Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded. +- verify-modes (REASONED; sqlite-rolling, coreutils): SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised. +- verify-listeners (REASONED; litefs, litestream, mcp, bash, curl): Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available. +- verify-turso (REASONED; turso, bash, curl): Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available. +- halt-expiration (REASONED; litefs): LiteFS checks halt-lock expiration every 5 seconds. + +````json +{ + "schema": 1, + "checked": "2026-10-17", + "documentation_checked": "2026-09", + "body_sha256": "1df2b900bb4797e8f2561a2cb2066c6c74bf8505a9043279c660e1d96229198f", + "components": { + "sqlite-rolling": { + "name": "SQLite documentation (rolling)", + "basis": "unknown", + "sources": { + "s2721da91f8d2": "https://www.sqlite.org/serverless.html", + "sf3d7f8391994": "https://www.sqlite.org/tempfiles.html", + "s742deb3338bb": "https://sqlite.org/loadext.html", + "sadf0431c1271": "https://www.sqlite.org/security.html" + } + }, + "mcp": { + "name": "Litestream MCP minimum", + "basis": "v0.5.0", + "sources": { + "sa5dddf04652c": "https://litestream.io/reference/config/" + } + }, + "see": { + "name": "SQLite Encryption Extension", + "basis": "unknown", + "sources": { + "sda3d8876503d": "https://www.sqlite.org/see/doc/trunk/www/readme.wiki" + } + }, + "turso": { + "name": "Turso documentation", + "basis": "unknown", + "sources": { + "sb32fd9c679e1": "https://docs.turso.tech/sdk/http/quickstart", + "sfec4e734f423": "https://docs.turso.tech/cli/db/tokens/create", + "se409838a474a": "https://docs.turso.tech/sdk/http/reference" + } + }, + "litestream": { + "name": "Litestream documentation", + "basis": "unknown", + "sources": { + "sec3d72af1447": "https://litestream.io/guides/", + "s87fa51accafc": "https://litestream.io/guides/s3/", + "sa5dddf04652c": "https://litestream.io/reference/config/" + } + }, + "cipher": { + "name": "SQLCipher", + "basis": "unknown", + "sources": { + "s57d83a42a74a": "https://www.zetetic.net/sqlcipher/" + } + }, + "litefs": { + "name": "LiteFS source", + "basis": "v0.5.14", + "sources": { + "s694a6ba8eafa": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L134-L268", + "s864cacb8e7cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L76-L99", + "s16da1761d6cc": "https://github.com/superfly/litefs/blob/v0.5.14/http/client.go#L32-L43", + "sf0e982d0e36a": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L57", + "s9389d639da6b": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L54-L58", + "s475c28fd949e": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L76-L110", + "s055e15e84ba5": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L219-L233", + "s0b0bac964067": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/config.go#L288-L333", + "s326a1007e5c2": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L294-L318", + "s4547cc32557d": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2779-L2811", + "s85ece0e83d43": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1904-L1910", + "sa467eeb47f34": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L320-L346", + "s5ffa5c20fc78": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2682-L2776", + "s02908966756d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L495-L520", + "sd63fa776fbc1": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L526-L585", + "s1c51b7efc42d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L686-L699", + "scd0168363c1a": "https://github.com/superfly/litefs/blob/v0.5.14/http/http.go#L15-L43", + "s298808a10d50": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L461-L493", + "s7269cca8b0ea": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2387-L2448", + "sbd59a1af784e": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L2452-L2609", + "sa199174f541b": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L348-L407", + "s750403129022": "https://github.com/superfly/litefs/blob/v0.5.14/db.go#L215-L325", + "s5009045b97e1": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1525-L1531", + "se2add54cf935": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L39-L41", + "s59a2dfccd853": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1455-L1465", + "s246e6ad0d330": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L409-L459", + "sae4ece3fbc87": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L408-L432", + "s7bcb006d6884": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L168-L170", + "se1a41376a98d": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L271-L292", + "s93c0ad91a567": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L779-L803", + "s36792457dd64": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L487-L488", + "s7234e34af9ef": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1661-L1713", + "s4cebe21e711d": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L329-L348", + "sb43f5d78723c": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L241-L244", + "s8dfa3f10f3d3": "https://github.com/superfly/litefs/blob/v0.5.14/lease.go#L99-L131", + "s83d8bd834347": "https://github.com/superfly/litefs/blob/v0.5.14/store.go#L1384-L1387", + "s3284d1a3ecad": "https://github.com/superfly/litefs/blob/v0.5.14/Dockerfile", + "s2b80fdf8adbe": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/mount_linux.go#L519-L524", + "sc0568fe7e0ee": "https://github.com/superfly/litefs/blob/v0.5.14/http/proxy_server.go#L131-L144", + "sf0d31f900082": "https://github.com/superfly/litefs/blob/v0.5.14/cmd/litefs/etc/litefs.yml#L60-L68", + "s82103ee03d64": "https://github.com/superfly/litefs/blob/v0.5.14/http/server.go#L32-L35" + } + }, + "litefs-docs": { + "name": "LiteFS documentation", + "basis": "unknown", + "sources": { + "s13b9e92d9996": "https://fly.io/docs/litefs/", + "sd7c0ba136b25": "https://fly.io/docs/litefs/config/" + } + }, + "go": { + "name": "Go library documentation", + "basis": "unknown", + "sources": { + "s6c5be8454523": "https://pkg.go.dev/github.com/golang/net/http2/h2c#NewHandler", + "s37204ff1b27c": "https://pkg.go.dev/net#Listen", + "se49529035a0c": "https://pkg.go.dev/expvar", + "s0246034dddd1": "https://pkg.go.dev/net/http/pprof" + } + }, + "docker": { + "name": "Docker documentation", + "basis": "unknown", + "sources": { + "s881082f899b2": "https://docs.docker.com/engine/network/drivers/bridge/", + "s6732393fd414": "https://docs.docker.com/engine/network/drivers/host/", + "s00095bd02553": "https://docs.docker.com/engine/network/#container-networks" + } + }, + "git": { + "name": "Git documentation", + "basis": "v2.56.0", + "sources": { + "s12aad6467212": "https://github.com/git/git/blob/v2.56.0/Documentation/gitignore.adoc#L15-L18", + "s8054ab09dc74": "https://github.com/git/git/blob/v2.56.0/Documentation/git-rm.adoc#L17-L28", + "s9a6802adec89": "https://github.com/git/git/blob/v2.56.0/Documentation/git-log.adoc#L16-L16" + } + }, + "coreutils": { + "name": "GNU Coreutils stat manual", + "basis": "v9.7", + "sources": { + "s247df4545dc0": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L12989-L12995", + "sfd49553ab6e5": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13046", + "sbd6f7cad7bf4": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13062", + "s3e55a7242c33": "https://github.com/coreutils/coreutils/blob/v9.7/doc/coreutils.texi#L13073" + } + }, + "bash": { + "name": "GNU Bash reference manual", + "basis": "bash-5.3", + "sources": { + "s3711969be580": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n450", + "s7fec9b923488": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n769", + "s77b19e384d70": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1397", + "se4f37e01a249": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n1866", + "s07f77f16f9f5": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2288", + "sbed8cd562a35": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2993", + "s7c9e44003867": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3131", + "s223b5c4d9a28": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4350", + "se02ece9163d4": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4582", + "s6067b2b1f78a": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5216", + "sa506f1abd6a1": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6182", + "s7d29125b4005": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n8232", + "s8962e63cfb20": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n2580", + "s8a27e686ddfb": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5466", + "s213f6d8d1f07": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3280", + "sf9757e9fd6e8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n3872", + "s8cbc1b4709e2": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4737", + "s3255eca7e496": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n4852", + "s252aa2e81179": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5156", + "s8447036b2114": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5161", + "s5cab49305f3d": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5548", + "s6e8ae812c1c3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5625", + "sb38bf74a89b0": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5629", + "s260459961fbc": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5659", + "s8c59508bd4c8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5917", + "s8016d1aecfca": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n5929", + "se62fd3ad4fc3": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6122", + "s2df39d2c9c1c": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n6194", + "s80e3246e96d8": "https://git.savannah.gnu.org/cgit/bash.git/tree/doc/bashref.texi?h=bash-5.3#n7274" + } + }, + "curl": { + "name": "curl", + "basis": "curl-8_14_1", + "sources": { + "s5b71321b79ea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/disable.md#L4-L20", + "sc405a4bffc27": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/globoff.md#L4-L22", + "s345ec65025b0": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/noproxy.md#L18-L28", + "sc9973c4ceec1": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/connect-timeout.md#L19-L28", + "s7b6dca376156": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/max-time.md#L21-L27", + "s156481f6b726": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/silent.md#L4-L25", + "s07916104bf60": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/show-error.md#L4-L19", + "sa937dbe5dd4a": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L4-L33", + "s021603e35e68": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L49-L53", + "s5e1383feef7d": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L75-L79", + "s39402f9cb25c": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/write-out.md#L105-L107", + "sf2dcd83157a6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/insecure.md#L4-L30", + "s13f463154cea": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/fail.md#L24-L28", + "s2f1ff2f6cacd": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/output.md#L4-L53", + "sd3b039bafbf6": "https://github.com/curl/curl/blob/curl-8_14_1/docs/cmdline-opts/data.md#L4-L49", + "s3db522258e44": "https://raw.githubusercontent.com/curl/curl/curl-8_14_1/docs/cmdline-opts/header.md" + } + }, + "grep-docs": { + "name": "GNU grep documentation", + "basis": "v3.12", + "sources": { + "s1733b640052b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n197", + "s99f121f5426a": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n342", + "sbc8d05ab659b": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n621", + "sfdd7a73eab39": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n757", + "s7f9dc35e81c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n788", + "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", + "sc08d6af4d642": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1142", + "scd528c854b46": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1150", + "sb8be8b716426": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1274", + "s5d5af9297df2": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1316", + "se99a83dba3c5": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1363" + } + } + }, + "claims": { + "filesystem": {"text": "No server, network listener or database-side authentication; filesystem authority controls access, including ATTACH. Treat files writable across security domains as suspect.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sadf0431c1271"], "status": "REASONED"}, + "extensions": {"text": "Core extension loading defaults off, while the CLI enables it; enable only when required and load trusted libraries.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s742deb3338bb"], "status": "REASONED"}, + "web-files": {"text": "Keep databases, sidecars, temporary files, exports and backups outside web-served paths; inspect aliases, symlinks, download routes and publishing, and do not rely on hidden names.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "git-files": {"text": "Ignore database extensions and each -wal, -shm and -journal suffix before committing; ignore rules do not remove tracked files or history. This guide assesses committed database rows as permanently available to anyone who clones, even after a later deletion commit.", "components": ["sqlite-rolling", "git"], "sources": ["sqlite-rolling:sf3d7f8391994", "git:s12aad6467212", "git:s8054ab09dc74", "git:s9a6802adec89"], "status": "REASONED"}, + "permissions": {"text": "Dedicated account, directory 700, database and existing sidecars 600, and launch umask 077; same-account processes and privileged users remain outside this isolation.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "temporary": {"text": "Protect temporary files and backups; Unix SQLITE_TMPDIR needs an existing private directory and may fall back elsewhere. Do not delete live sidecars to repair permissions.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "encryption": {"text": "Public-domain SQLite has no file encryption; SEE is licensed and SQLCipher is a third-party alternative, otherwise use filesystem or volume encryption.", "components": ["see", "cipher"], "sources": ["see:sda3d8876503d", "cipher:s57d83a42a74a"], "status": "REASONED"}, + "encryption-keys": {"text": "Keep keys outside databases and backups; SEE TEMP tables can be unencrypted. Treat credential-bearing copies as secrets and rotate credentials after a leak.", "components": ["see", "sqlite-rolling"], "sources": ["see:sda3d8876503d", "sqlite-rolling:sf3d7f8391994"], "status": "REASONED"}, + "turso-http": {"text": "Turso serves libSQL over HTTPS at the database/organization turso.io URL using bearer authentication; TURSO_DATABASE_URL and TURSO_AUTH_TOKEN belong in application configuration, never a client bundle or git.", "components": ["turso"], "sources": ["turso:sb32fd9c679e1"], "status": "REASONED"}, + "turso-scope": {"text": "db tokens create --read-only scopes away writes; --expiration accepts never or durations such as 7d3h. Prefer scoped, expiring tokens.", "components": ["turso"], "sources": ["turso:sfec4e734f423"], "status": "REASONED"}, + "replicas": {"text": "Litestream replicates to supported object stores; scope AWS credentials and IAM to the required bucket/prefix, keep replicas private, and restore with scoped credentials.", "components": ["litestream"], "sources": ["litestream:sec3d72af1447", "litestream:s87fa51accafc"], "status": "REASONED"}, + "metrics": {"text": "Litestream addr metrics listener defaults disabled; leave off unless needed, otherwise bind loopback, for example 127.0.0.1:9090.", "components": ["litestream"], "sources": ["litestream:sa5dddf04652c"], "status": "REASONED"}, + "mcp": {"text": "Litestream MCP mcp-addr is documented for v0.5.0 and later, defaults disabled and has no built-in authentication; protect information and restore access with an authenticated tunnel/proxy, for example at loopback:3001.", "components": ["mcp"], "sources": ["mcp:sa5dddf04652c"], "status": "REASONED"}, + "litefs-backups": {"text": "LiteFS replicates among cluster nodes; its pre-1.0 documentation recommends separate off-site backups with private destinations.", "components": ["litefs-docs"], "sources": ["litefs-docs:s13b9e92d9996"], "status": "REASONED"}, + "litefs-transport": {"text": "LiteFS v0.5.14 routes have no credential gate; the server accepts cleartext HTTP/1 and h2c, and its client uses h2c. Litefs-Id comparisons reject self-connections, not unauthenticated callers.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc", "litefs:s16da1761d6cc", "go:s6c5be8454523"], "status": "REASONED"}, + "litefs-bind": {"text": "http.addr defaults to host-less :20202, including the example; an empty address still listens on all interfaces at a system-chosen port.", "components": ["litefs", "go"], "sources": ["litefs:sf0e982d0e36a", "litefs:s9389d639da6b", "go:s37204ff1b27c", "litefs:s82103ee03d64"], "status": "REASONED"}, + "litefs-config": {"text": "Set API bind in the config file; no direct address flag. Search working directory, available home directory, then /etc/litefs.yml; -config overrides and -no-expand-env disables ${VAR} expansion.", "components": ["litefs"], "sources": ["litefs:s475c28fd949e", "litefs:s055e15e84ba5", "litefs:s0b0bac964067"], "status": "REASONED"}, + "import": {"text": "POST /import creates or replaces a named database on the primary only; replicas return 503 before creation without forwarding, and DB.Import also checks primary status.", "components": ["litefs"], "sources": ["litefs:s326a1007e5c2", "litefs:s4547cc32557d", "litefs:s85ece0e83d43"], "status": "REASONED"}, + "export": {"text": "GET /export returns the full existing local database, including applicable WAL pages, on either role; missing databases return 404.", "components": ["litefs"], "sources": ["litefs:sa467eeb47f34", "litefs:s5ffa5c20fc78"], "status": "REASONED"}, + "stream": {"text": "POST /stream requires primary, HTTP/2 and a valid position map; replicas return 503. Unfiltered streams cover every database, filters narrow them, and fresh positions receive snapshots.", "components": ["litefs"], "sources": ["litefs:s02908966756d", "litefs:sd63fa776fbc1", "litefs:s1c51b7efc42d", "litefs:scd0168363c1a"], "status": "REASONED"}, + "tx": {"text": "POST /tx applies a validated LTX file to an existing local database on either role without forwarding, primary or halt-owner checks; non-snapshots must match next transaction/checksum, missing databases return 404.", "components": ["litefs"], "sources": ["litefs:s298808a10d50", "litefs:s7269cca8b0ea", "litefs:sbd59a1af784e"], "status": "REASONED"}, + "halt": {"text": "POST /halt on either role creates the database if absent and acquires write locks with a nonzero id, stalling primary writes or replica updates; DELETE releases an existing lock for a matching id.", "components": ["litefs"], "sources": ["litefs:sa199174f541b", "litefs:s750403129022", "litefs:s5009045b97e1"], "status": "REASONED"}, + "halt-ttl": {"text": "LiteFS halt-lock TTL defaults to 30 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"}, + "promote": {"text": "POST /promote needs an eligible candidate, succeeds unchanged for an existing primary, and asks a known primary for handoff from a candidate replica; no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:sae4ece3fbc87"], "status": "REASONED"}, + "handoff": {"text": "POST /handoff requires primary, a connected target subscriber and a handoff-capable lease; static leases refuse it and no database name is required.", "components": ["litefs"], "sources": ["litefs:s246e6ad0d330", "litefs:s7bcb006d6884"], "status": "REASONED"}, + "information": {"text": "GET /info, /events and /metrics disclose node, cluster and database state on either role without a named database.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:se1a41376a98d", "litefs:s93c0ad91a567"], "status": "REASONED"}, + "debug": {"text": "Unconditional /debug/pprof/ exposes command line and live profiles; /debug/vars exposes command line, memory and store database/lock state on either role without a database name.", "components": ["litefs", "go"], "sources": ["litefs:s694a6ba8eafa", "litefs:s36792457dd64", "litefs:s7234e34af9ef", "go:se49529035a0c", "go:s0246034dddd1"], "status": "REASONED"}, + "debug-rand": {"text": "/debug/rand streams deterministic pseudo-random data; its minute timeout is checked between writes and no server write timeout bounds a slow or non-reading client.", "components": ["litefs"], "sources": ["litefs:s694a6ba8eafa", "litefs:s864cacb8e7cc"], "status": "REASONED"}, + "cluster-private": {"text": "Bind each LiteFS API to its private node address and restrict reachability to cluster nodes; never publish 20202 through an internet-facing mapping or proxy.", "components": ["litefs", "litefs-docs"], "sources": ["litefs:s694a6ba8eafa", "litefs-docs:sd7c0ba136b25"], "status": "REASONED"}, + "consul-url": {"text": "Consul lease.advertise-url names each node private API; when unset it derives http:// plus lease.hostname or OS hostname and the actual listening port.", "components": ["litefs"], "sources": ["litefs:s4cebe21e711d"], "status": "REASONED"}, + "static-url": {"text": "Static leasing uses lease.advertise-url as configured: the primary points at itself and each replica at the primary private API.", "components": ["litefs"], "sources": ["litefs:s8dfa3f10f3d3", "litefs:s83d8bd834347", "litefs:sb43f5d78723c"], "status": "REASONED"}, + "container": {"text": "LiteFS image supplies no configuration or EXPOSE; default wildcard binding applies within its namespace. Peers, host/shared networking, routing, firewall and publication determine reachability.", "components": ["litefs", "docker"], "sources": ["litefs:s3284d1a3ecad", "docker:s881082f899b2", "docker:s6732393fd414", "docker:s00095bd02553"], "status": "REASONED"}, + "proxy": {"text": "LiteFS application proxy is separate: proxy.target enables attempted startup, proxy.db and proxy.addr must be nonempty, example :8080; application TLS and authentication remain necessary.", "components": ["litefs"], "sources": ["litefs:s2b80fdf8adbe", "litefs:sc0568fe7e0ee", "litefs:sf0d31f900082"], "status": "REASONED"}, + "verify-download": {"text": "Probe actual database/sidecar paths only after a successful application control; timeouts, DNS and proxy failures do not establish non-publication. No served application was available.", "components": ["sqlite-rolling", "bash", "curl"], "sources": ["sqlite-rolling:s2721da91f8d2", "sqlite-rolling:sf3d7f8391994", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [1]}, + "verify-bundle": {"text": "Local placeholder-token scan distinguished exposed and clean bundles and refused absent directories; readonly, nameref, lowercase and IFS cases are recorded. A clean scan covers only the searched forms and paths.", "components": ["turso", "bash", "grep-docs"], "sources": ["turso:sb32fd9c679e1", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "grep-docs:s1733b640052b", "grep-docs:s99f121f5426a", "grep-docs:sbc8d05ab659b", "grep-docs:sfdd7a73eab39", "grep-docs:s7f9dc35e81c5", "grep-docs:s5170317ddea8", "grep-docs:sc08d6af4d642", "grep-docs:scd528c854b46", "grep-docs:sb8be8b716426", "grep-docs:s5d5af9297df2", "grep-docs:se99a83dba3c5"], "status": "DEMONSTRATED", "evidence": "A bundle carrying the placeholder printed `dist/app.js` before `token-literal exit: 0` and again before `jwt-shape exit: 0`, and a clean bundle printed `1` for both.", "verify": [2]}, + "verify-git": {"text": "Local throwaway repositories showed tracked/history copies survive ignore rules and index removal; a never-committed ignored file produced only rule matches. Git version is unrecorded.", "components": ["sqlite-rolling"], "sources": ["sqlite-rolling:sf3d7f8391994"], "status": "DEMONSTRATED", "evidence": "With `app.db` committed before an `app.db*` ignore rule was added, `git check-ignore -v` listed the sidecars but not the tracked `app.db`. `git ls-files` printed `app.db`, and `git log` printed its commit.", "verify": [3]}, + "verify-modes": {"text": "SQLite 3.46.1 local WAL and rollback-journal runs showed umask 022 versus 077 modes; every file belonged to the test account, and separate application-account ownership was not exercised.", "components": ["sqlite-rolling", "coreutils"], "sources": ["sqlite-rolling:sf3d7f8391994", "coreutils:s247df4545dc0", "coreutils:sfd49553ab6e5", "coreutils:sbd6f7cad7bf4", "coreutils:s3e55a7242c33"], "status": "REASONED", "verify": [4]}, + "verify-listeners": {"text": "Compare authorized private control with untrusted reachability for LiteFS or enabled Litestream metrics/MCP; any HTTP response shows a listener, not every backend route. Transport errors and failed controls are inconclusive; no isolated listener environment was available.", "components": ["litefs", "litestream", "mcp", "bash", "curl"], "sources": ["litefs:s694a6ba8eafa", "litestream:sa5dddf04652c", "mcp:sa5dddf04652c", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:s2f1ff2f6cacd"], "status": "REASONED", "verify": [5]}, + "verify-turso": {"text": "Turso /v2/pipeline must return SELECT 1 with a valid bearer token and no successful result anonymously; HTTP 200 alone is insufficient and transport failure inconclusive. No database/token was available.", "components": ["turso", "bash", "curl"], "sources": ["turso:se409838a474a", "bash:s3711969be580", "bash:s7fec9b923488", "bash:s77b19e384d70", "bash:se4f37e01a249", "bash:s07f77f16f9f5", "bash:sbed8cd562a35", "bash:s7c9e44003867", "bash:s223b5c4d9a28", "bash:se02ece9163d4", "bash:s6067b2b1f78a", "bash:sa506f1abd6a1", "bash:s7d29125b4005", "bash:s8962e63cfb20", "bash:s8a27e686ddfb", "bash:s213f6d8d1f07", "bash:sf9757e9fd6e8", "bash:s8cbc1b4709e2", "bash:s3255eca7e496", "bash:s252aa2e81179", "bash:s8447036b2114", "bash:s5cab49305f3d", "bash:s6e8ae812c1c3", "bash:sb38bf74a89b0", "bash:s260459961fbc", "bash:s8c59508bd4c8", "bash:s8016d1aecfca", "bash:se62fd3ad4fc3", "bash:s2df39d2c9c1c", "bash:s80e3246e96d8", "curl:s5b71321b79ea", "curl:sc405a4bffc27", "curl:s345ec65025b0", "curl:sc9973c4ceec1", "curl:s7b6dca376156", "curl:s156481f6b726", "curl:s07916104bf60", "curl:sa937dbe5dd4a", "curl:s021603e35e68", "curl:s5e1383feef7d", "curl:s39402f9cb25c", "curl:sf2dcd83157a6", "curl:s13f463154cea", "curl:sd3b039bafbf6", "curl:s3db522258e44"], "status": "REASONED", "verify": [6]}, + "halt-expiration": {"text": "LiteFS checks halt-lock expiration every 5 seconds.", "components": ["litefs"], "sources": ["litefs:se2add54cf935", "litefs:s59a2dfccd853"], "status": "REASONED"} + } +} +```` + diff --git a/transactional-email-posture.md b/transactional-email-posture.md index 8f3d7639f..4b06f8214 100644 --- a/transactional-email-posture.md +++ b/transactional-email-posture.md @@ -1,5 +1,238 @@ ---- -version_basis: { +# Transactional email posture: authenticating the mail your deployment sends + +Written against SPF RFC 7208, DKIM RFC 6376, DKIM cryptography RFC 8301, DMARC RFC 9989, DMARC aggregate reporting RFC 9990, DMARC failure reporting RFC 9991, Submission TLS RFC 8314, and SMTP submission roles RFC 6409, plus 7 more recorded bases; 1 further source carries no recorded version. Metadata reviewed 2026-10-05; 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. + +Password resets, sign-in links, and invitations are part of your authentication path: a recipient who trusts a forged one hands an attacker the account. The controls that stop that forgery are not a single service's settings but three DNS-published standards (SPF, DKIM, and DMARC), the submission channel your application sends through, and the transport policy between mail servers. This guide covers that posture for a deployment that sends transactional mail. It is standards-based rather than product-based, so [authentication.md](authentication.md) covers the account side and [secrets.md](secrets.md) covers the SMTP credential and DKIM private key you will handle here. + +The through-line: a spoofable sender undermines every message your users are trained to act on, an open relay burns your sending reputation and gets your mail blocked, and an authenticated but plaintext submission leaks the credential that lets an attacker send as you. All values below (domains, IPs, selectors, keys) are illustrative; replace them with your own deployment inventory. + +## 1. SPF: authorize the sending IPs + +SPF publishes, as a DNS TXT record, the IP addresses allowed to send for a domain. A receiver checks the record against the connecting IP and the envelope `MAIL FROM` domain (the `HELO` name for a null return path); it does not check the visible `From:` header, which is why SPF alone does not stop header spoofing and only becomes useful once DMARC ties it to `From:`. + +```dns +bounce.example.com. 3600 IN TXT "v=spf1 ip4:192.0.2.10 include:_spf.provider.example -all" +``` + +The final mechanism decides unmatched senders: `-all` fails them, `~all` softfails (many receivers still deliver a softfail), and `+all` or a bare `all` passes everyone, which destroys the record's meaning. Publish exactly one SPF record per evaluated domain; a domain with two `v=spf1` records is a `permerror`. SPF also caps the terms that trigger DNS lookups (`include`, `a`, `mx`, `ptr`, `exists`, and the `redirect` modifier) at ten across the whole recursion, and exceeding that is a `permerror` that voids the result; `ip4`, `ip6`, and `all` do not count against the ten (RFC 7208 sections 4.5 and 4.6.4). A long chain of provider `include`s is the usual way a domain silently blows the budget: any evaluation that has to walk the whole chain returns `permerror`, though a sender matched by an earlier mechanism such as a listed `ip4` still passes, because evaluation stops at the first match. A `permerror` is not an SPF pass, so legitimate mail that reached it and relied on SPF alignment fails your own DMARC. It breaks your authentication, not an attacker's. + +What remains unverified until you inspect your own zone: the real envelope domain, the provider `include` targets, the live recursive lookup count, and whether a second SPF record was left behind by an earlier vendor. + +## 2. DKIM: sign with a rotatable key + +DKIM attaches a cryptographic signature to each message and publishes the matching public key at a selector under the signing domain. The signature names its domain as `d=` and its selector as `s=`; the receiver fetches the key from the selector record under `_domainkey` (here `tx202609._domainkey.example.com`) to verify it. DKIM proves the signed headers and body were not altered and were authorized by the `d=` domain; it does not encrypt the message. + +```dns +tx202609._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." +``` + +The `p=` value above is a truncated illustrative key; publish the full base64 your signer generates. Sign with `rsa-sha256`: RFC 8301 prohibits `rsa-sha1` outright, sets a hard floor of 1024-bit RSA keys, and recommends 2048-bit or larger, so treat 2048-bit as the working minimum rather than the 1024-bit floor. Rotate by publishing a new selector's public key first, switching signing to it, and only then retiring the old selector once mail signed with it has cleared the network; an empty `p=` revokes a key. Publishing a key is not enough on its own: the sending service must actually sign, and a receiver must actually verify, before DKIM contributes to a DMARC pass. + +Unverified until checked in your deployment: whether signing is enabled, the live `d=` and selector, the key length, whether the key is a TXT record or a provider CNAME delegation, and who owns rotation. + +## 3. DMARC: align authentication to the visible From and enforce + +DMARC is the record that finally protects the `From:` your users see. A message passes DMARC when at least one of SPF or DKIM both passes and is aligned: SPF alignment compares the envelope domain, DKIM alignment compares the verified signature's `d=`, each against the visible `From:` domain. Relaxed alignment (`adkim=r`, `aspf=r`, the default) accepts the same organizational domain; strict (`s`) requires an exact match. + +```dns +_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; sp=reject; np=reject; adkim=r; aspf=r; rua=mailto:dmarc-agg@example.com" +``` + +`p=` sets the requested handling of failures: `none` asks for no change (monitor only, and still spoofable), `quarantine` asks receivers to treat failures as suspicious, and `reject` asks them to reject at SMTP time. `sp=` sets the policy for existing subdomains and `np=` for non-existent ones; where a tag is absent, policy falls back `np` to `sp` to `p`, so a `p=reject` domain already rejects for its own subdomains without them. Publish `sp` and `np` when you want subdomains held to a policy of their own, whether it repeats or differs from `p`, and inventory any subdomain that publishes its own DMARC record, since a receiver's tree walk finds that record first. DMARC discovers the applicable policy and organizational domain by a DNS tree walk (RFC 9989 section 4.10), which replaced the old public-suffix-list method. `t=y` weakens enforcement for testing and `t=n` is the default; the enforcement record above omits it. + +Reach `p=reject` only after a monitoring period under `p=none` with aggregate reports confirms every legitimate stream aligns, or you will reject your own mail. Missing DMARC or `p=none` means no enforcement is requested, not that mail is guaranteed to arrive; receivers keep their own local policy. One legacy note: RFC 9989 (May 2026), which now defines DMARC and obsoletes RFC 7489, removed the `pct` tag, so a `pct` value is not a current rollout control, and a legacy `p=reject; pct=25` was partial application rather than full enforcement. + +Unverified until checked: which streams actually align, the inherited and explicit subdomain policies, and whether your receivers implement the current specification. + +## 4. DMARC reporting: aggregate now, failure reports later + +Aggregate reporting (`rua=`, RFC 9990) sends a daily XML summary of authentication results and is how you see what is sending as your domain before you enforce; turn it on first. A destination in another domain must authorize the reports, or receivers will not send them: + +```dns +example.com._report._dmarc.collector.example.net. IN TXT "v=DMARC1" +``` + +Failure reporting (`ruf=`, RFC 9991) sends per-message forensic copies that can include message content and recipient data, which for password-reset mail can mean leaking a live reset token to whatever inbox collects the reports. Omit `ruf` at first, and enable it only after you have settled collection, redaction, retention, and access, and authorized any external destination the same way. Publication alone is not evidence reports arrive: verify the mailbox actually receives and processes them. + +## 5. SMTP submission and relay: TLS before AUTH, and never an open relay + +Where your application hands mail to a mail server, three ports carry different contracts. Port 587 is submission with STARTTLS: the client upgrades to TLS and then authenticates, but the STARTTLS offer alone does not force encryption, so an opportunistic client that falls back to plaintext is exposed to a STARTTLS-stripping downgrade that captures the credential. Port 465 is submission with implicit TLS: the connection is encrypted from the first byte and is immune to stripping, which is why RFC 8314 prefers it going forward. Require successful TLS with certificate chain and hostname validation before any credential is sent on either, and treat TLS 1.2 as the floor (RFC 8997). Port 25 is inter-server relay or, at some providers, submission; "port 25 means no authentication" is wrong (RFC 6409 section 3.1), so identify which role your endpoint plays rather than assuming. + +An open relay forwards mail from an unauthenticated, untrusted client to a destination the server is not responsible for; it will be found and abused, and your domain blocked. Reject that forwarding while still allowing the two legitimate cases: ordinary unauthenticated inbound delivery to your own local recipients, and explicitly authorized relay paths (RFC 2505 section 2.1). After TLS, an unauthenticated or invalidly authenticated submission must fail and an authorized one succeed. + +Unverified until checked: the live endpoints and ports, whether the client enforces TLS rather than merely offering it, the authentication mechanism, and the relay access rules. + +## 6. Transport and display: MTA-STS, TLS-RPT, and BIMI + +MTA-STS (RFC 8461) lets a receiving domain demand TLS for mail sent to it. A TXT record points to an HTTPS-hosted policy: + +```dns +_mta-sts.example.com. IN TXT "v=STSv1; id=2026091501" +``` + +The policy is served as `text/plain` at `https://mta-sts.example.com/.well-known/mta-sts.txt`: + +```text +version: STSv1 +mode: enforce +mx: mx1.example.com +max_age: 604800 +``` + +Only `mode: enforce` constrains delivery (`testing` and `none` do not), and the crucial caveat is direction: MTA-STS protects mail arriving at the policy domain, so publishing it does not secure your own outbound mail to other domains, and it depends on your sending server validating recipients' policies. Update the `id` whenever the policy changes. TLS-RPT (RFC 8460) is reporting only, never enforcement: + +```dns +_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-rpt@example.com" +``` + +BIMI is optional display metadata that shows a brand logo in supporting clients; it depends on DMARC enforcement already being in place and is not an authentication mechanism or a guarantee the logo renders. Keep it last, after the records above are enforcing. + +## Verify + +REASONED: following block; DNS publication, submission TLS and plaintext AUTH refusal, per the guide's cited BIND 9.18.39 dig manual, OpenSSL 3.0 s_client, RFC 8314: Cleartext Considered Obsolete and RFC 8997 section 3. No controlled DNS zone or SMTP endpoint is available, and local listeners are denied. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; openssl s_client -help exited 0 and listed -starttls, -verify_hostname, -verify_return_error and -min_protocol. Published records should match the configured policies; exposed submission accepts plaintext AUTH, while fixed submission refuses it and supports validated TLS before authorized AUTH. These live outcomes remain unobserved. Every probe below is reasoned, not demonstrated: the authoring environment has no DNS control, no SMTP endpoint, and no container runtime, so the outcomes are derived from the cited RFCs rather than observed. For the submission probes, a transport failure or a TLS error is inconclusive, never the fixed state. + +```bash +# REASONED: DNS and TLS expectations follow the cited RFCs; no DNS control, SMTP endpoint, or container runtime is available. +# Inspect the published records (replace names; take the DKIM selector from a real signature). +# These read DNS and TLS state; they do not evaluate SPF or DMARC or prove a service listener. +dig +noall +answer TXT bounce.example.com +dig +noall +answer TXT tx202609._domainkey.example.com +dig +noall +answer CNAME tx202609._domainkey.example.com +dig +noall +answer TXT _dmarc.example.com +dig +noall +answer TXT _mta-sts.example.com +dig +noall +answer TXT _smtp._tls.example.com +dig +noall +answer TXT default._bimi.example.com + +# Submission must complete TLS with a validated certificate before AUTH. -verify_return_error +# matters because s_client otherwise continues past a verification failure. This confirms TLS is +# available, not that AUTH is gated on it; the discriminating check, reasoned here, is the negative +# one: a plaintext or post-stripped-STARTTLS AUTH attempt must be refused, where the exposed state +# accepts it. +openssl s_client -connect smtp.example.com:587 -starttls smtp \ + -servername smtp.example.com -verify_hostname smtp.example.com \ + -verify_return_error -min_protocol TLSv1.2 +## 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/transactional-email-posture.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): + +- spf: SPF (RFC 7208) +- dkim: DKIM (RFC 6376) +- crypto: DKIM cryptography (RFC 8301) +- dmarc: DMARC (RFC 9989) +- aggregate: DMARC aggregate reporting (RFC 9990) +- failure: DMARC failure reporting (RFC 9991) +- submission: Submission TLS (RFC 8314) +- roles: SMTP submission roles (RFC 6409) +- relay: SMTP relay controls (RFC 2505) +- sts: MTA-STS (RFC 8461) +- rpt: TLS-RPT (RFC 8460) +- results: Authentication-Results (RFC 8601) +- bimi: BIMI documentation (unknown) +- submission-update: Submission TLS update (RFC 8997) +- bind: BIND dig manual (9.18.39) +- openssl: OpenSSL command documentation (3.0) + +Claims (full source mapping in the JSON block below): + +- spf-identity (REASONED; spf, dmarc): SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment. +- spf-all (REASONED; spf): -all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone. +- spf-record (REASONED; spf): Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror. +- spf-lookups (REASONED; spf): Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass. +- dkim-signing (REASONED; dkim, dmarc): DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC. +- dkim-crypto (REASONED; crypto): Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example. +- dkim-rotation (REASONED; dkim): Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership. +- dmarc-alignment (REASONED; dmarc): DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching. +- dmarc-policy (REASONED; dmarc): p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery. +- dmarc-subdomains (REASONED; dmarc): sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies. +- dmarc-discovery (REASONED; dmarc): RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method. +- dmarc-testing (REASONED; dmarc): t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag. +- dmarc-rollout (REASONED; dmarc, aggregate): Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected. +- dmarc-pct (REASONED; dmarc): RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control. +- aggregate (REASONED; aggregate): Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked. +- failure-reports (REASONED; failure): ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations. +- starttls (REASONED; submission): 587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS. +- implicit-tls (REASONED; submission): 465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward. +- tls-validation (REASONED; submission, submission-update): Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997. +- port-25 (REASONED; roles): 25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication. +- relay-policy (REASONED; relay): Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths. +- submission-auth (REASONED; roles, submission): After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds. +- sts-publication (REASONED; sts): MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800. +- sts-enforcement (REASONED; sts): Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail. +- sts-refresh (REASONED; sts): Update the MTA-STS TXT id whenever the policy changes. +- tls-reports (REASONED; rpt): TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement. +- bimi (REASONED; bimi): BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last. +- verify-dns (REASONED; spf, dkim, dmarc, sts, rpt, bimi, bind): Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available. +- verify-tls (REASONED; submission, submission-update, openssl): 587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available. +- verify-alignment (REASONED; results, dmarc): Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling. +- verify-relay (REASONED; relay, roles): In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved. + +````json +{ "schema": 1, "checked": "2026-10-05", "documentation_checked": "2026-09", @@ -153,212 +386,5 @@ version_basis: { "verify-relay": {"text": "In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved.", "components": ["relay", "roles"], "sources": ["relay:s1b9349dd99bc", "roles:s3f9972faf8ff"], "status": "REASONED"} } } ---- -# Transactional email posture: authenticating the mail your deployment sends - - -**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): - -- spf: SPF (RFC 7208) -- dkim: DKIM (RFC 6376) -- crypto: DKIM cryptography (RFC 8301) -- dmarc: DMARC (RFC 9989) -- aggregate: DMARC aggregate reporting (RFC 9990) -- failure: DMARC failure reporting (RFC 9991) -- submission: Submission TLS (RFC 8314) -- roles: SMTP submission roles (RFC 6409) -- relay: SMTP relay controls (RFC 2505) -- sts: MTA-STS (RFC 8461) -- rpt: TLS-RPT (RFC 8460) -- results: Authentication-Results (RFC 8601) -- bimi: BIMI documentation (unknown) -- submission-update: Submission TLS update (RFC 8997) -- bind: BIND dig manual (9.18.39) -- openssl: OpenSSL command documentation (3.0) - -Claims (full source mapping in the front matter): - -- spf-identity (REASONED; spf, dmarc): SPF TXT authorizes connecting IPs for MAIL FROM, or HELO with a null return path; it does not authenticate visible From without DMARC alignment. -- spf-all (REASONED; spf): -all fails unmatched senders, ~all softfails and may still be delivered, while +all or bare all passes everyone. -- spf-record (REASONED; spf): Publish exactly one v=spf1 record per evaluated domain; multiple records produce permerror. -- spf-lookups (REASONED; spf): Ten DNS-lookup terms across recursion include include/a/mx/ptr/exists/redirect but exclude ip4/ip6/all; exceeding the limit gives permerror. An earlier matching mechanism stops evaluation; permerror is no SPF pass. -- dkim-signing (REASONED; dkim, dmarc): DKIM d= and s= select the _domainkey public key; signatures authenticate signed content for the signing domain, not encryption. Actual signing and verification are required for DMARC. -- dkim-crypto (REASONED; crypto): Use rsa-sha256, never rsa-sha1; RFC 8301 sets a 1024-bit RSA floor and recommends at least 2048 bits, used here as the working minimum. Publish the full generated key, not the truncated example. -- dkim-rotation (REASONED; dkim): Publish a new selector before switching signing, retire the old selector after in-flight mail clears, and revoke with empty p=; inspect actual selector, delegation and rotation ownership. -- dmarc-alignment (REASONED; dmarc): DMARC requires at least one passing and aligned SPF or DKIM result; relaxed adkim=r/aspf=r defaults to organizational-domain alignment, strict s requires exact domain matching. -- dmarc-policy (REASONED; dmarc): p=none requests monitoring only, quarantine suspicious handling, and reject SMTP-time rejection; receivers retain local policy, so missing enforcement does not guarantee delivery. -- dmarc-subdomains (REASONED; dmarc): sp covers existing subdomains and np non-existent ones, falling back np to sp to p; a subdomain-specific record can take precedence. Inventory explicit and inherited policies. -- dmarc-discovery (REASONED; dmarc): RFC 9989 DNS tree walk discovers policy and organizational domain, replacing the former public-suffix-list method. -- dmarc-testing (REASONED; dmarc): t=y weakens enforcement and t=n is the default; the illustrated enforcing record omits the tag. -- dmarc-rollout (REASONED; dmarc, aggregate): Monitor aggregate reports under p=none until legitimate streams align before moving to p=reject, or legitimate mail can be rejected. -- dmarc-pct (REASONED; dmarc): RFC 9989, May 2026, obsoletes RFC 7489 and removes pct; legacy p=reject;pct=25 was partial application, not a current rollout control. -- aggregate (REASONED; aggregate): Enable rua daily aggregate XML reporting to observe sending streams; an external reporting domain must authorize reports, and actual receipt/processing must be checked. -- failure-reports (REASONED; failure): ruf forensic reports can expose content, recipients and reset tokens; omit initially, settle redaction/retention/access before enabling, and authorize external destinations. -- starttls (REASONED; submission): 587 submission upgrades with STARTTLS before authentication; an opportunistic fallback can expose credentials to stripping, so require successful validated TLS. -- implicit-tls (REASONED; submission): 465 submission starts with TLS and avoids STARTTLS stripping; RFC 8314 prefers implicit TLS going forward. -- tls-validation (REASONED; submission, submission-update): Validate certificate chain and hostname before credentials on either submission port; the guide states TLS 1.2 as the floor and attributes that update to RFC 8997. -- port-25 (REASONED; roles): 25 is inter-server relay or provider submission; identify the endpoint role instead of assuming that port 25 implies no authentication. -- relay-policy (REASONED; relay): Reject unauthenticated untrusted forwarding to non-local recipients while allowing ordinary inbound local delivery and explicitly authorized relay paths. -- submission-auth (REASONED; roles, submission): After TLS, unauthenticated or invalidly authenticated submission must fail while authorized submission succeeds. -- sts-publication (REASONED; sts): MTA-STS TXT advertises the HTTPS text/plain policy at /.well-known/mta-sts.txt on mta-sts.example.com; the example selects enforce, an MX host and max_age 604800. -- sts-enforcement (REASONED; sts): Only enforce constrains delivery; testing and none do not. MTA-STS protects mail arriving at the policy domain and depends on sending-server validation, not protection of the publishing domain outbound mail. -- sts-refresh (REASONED; sts): Update the MTA-STS TXT id whenever the policy changes. -- tls-reports (REASONED; rpt): TLS-RPT _smtp._tls TXT with TLSRPTv1 and rua supplies reports, never enforcement. -- bimi (REASONED; bimi): BIMI is optional logo display metadata requiring DMARC enforcement, not authentication or guaranteed rendering; deploy it last. -- verify-dns (REASONED; spf, dkim, dmarc, sts, rpt, bimi, bind): Inspect SPF, DKIM TXT/CNAME, DMARC, MTA-STS, TLS-RPT and BIMI records using actual inventory; DNS inspection alone does not evaluate SPF/DMARC or prove a listener. No DNS control was available. -- verify-tls (REASONED; submission, submission-update, openssl): 587 STARTTLS and 465 implicit-TLS probes require validated TLS 1.2 or newer; TLS availability does not establish AUTH gating. Plaintext or stripped-STARTTLS AUTH must be refused; transport/TLS failures are inconclusive. No SMTP endpoint or container runtime was available. -- verify-alignment (REASONED; results, dmarc): Send a benign message to a controlled recipient and inspect receiver-trusted Authentication-Results, not a supplied header; require passing aligned SPF or DKIM and p=reject with subdomain coverage and no testing or legacy sampling. -- verify-relay (REASONED; relay, roles): In an operator-controlled environment, external MAIL FROM and non-local RCPT TO should receive 5xx; compare with authorized submission and ordinary local delivery. These expected outcomes remain unobserved. +```` - -Password resets, sign-in links, and invitations are part of your authentication path: a recipient who trusts a forged one hands an attacker the account. The controls that stop that forgery are not a single service's settings but three DNS-published standards (SPF, DKIM, and DMARC), the submission channel your application sends through, and the transport policy between mail servers. This guide covers that posture for a deployment that sends transactional mail. It is standards-based rather than product-based, so [authentication.md](authentication.md) covers the account side and [secrets.md](secrets.md) covers the SMTP credential and DKIM private key you will handle here. - -The through-line: a spoofable sender undermines every message your users are trained to act on, an open relay burns your sending reputation and gets your mail blocked, and an authenticated but plaintext submission leaks the credential that lets an attacker send as you. All values below (domains, IPs, selectors, keys) are illustrative; replace them with your own deployment inventory. - -## 1. SPF: authorize the sending IPs - -SPF publishes, as a DNS TXT record, the IP addresses allowed to send for a domain. A receiver checks the record against the connecting IP and the envelope `MAIL FROM` domain (the `HELO` name for a null return path); it does not check the visible `From:` header, which is why SPF alone does not stop header spoofing and only becomes useful once DMARC ties it to `From:`. - -```dns -bounce.example.com. 3600 IN TXT "v=spf1 ip4:192.0.2.10 include:_spf.provider.example -all" -``` - -The final mechanism decides unmatched senders: `-all` fails them, `~all` softfails (many receivers still deliver a softfail), and `+all` or a bare `all` passes everyone, which destroys the record's meaning. Publish exactly one SPF record per evaluated domain; a domain with two `v=spf1` records is a `permerror`. SPF also caps the terms that trigger DNS lookups (`include`, `a`, `mx`, `ptr`, `exists`, and the `redirect` modifier) at ten across the whole recursion, and exceeding that is a `permerror` that voids the result; `ip4`, `ip6`, and `all` do not count against the ten (RFC 7208 sections 4.5 and 4.6.4). A long chain of provider `include`s is the usual way a domain silently blows the budget: any evaluation that has to walk the whole chain returns `permerror`, though a sender matched by an earlier mechanism such as a listed `ip4` still passes, because evaluation stops at the first match. A `permerror` is not an SPF pass, so legitimate mail that reached it and relied on SPF alignment fails your own DMARC. It breaks your authentication, not an attacker's. - -What remains unverified until you inspect your own zone: the real envelope domain, the provider `include` targets, the live recursive lookup count, and whether a second SPF record was left behind by an earlier vendor. - -## 2. DKIM: sign with a rotatable key - -DKIM attaches a cryptographic signature to each message and publishes the matching public key at a selector under the signing domain. The signature names its domain as `d=` and its selector as `s=`; the receiver fetches the key from the selector record under `_domainkey` (here `tx202609._domainkey.example.com`) to verify it. DKIM proves the signed headers and body were not altered and were authorized by the `d=` domain; it does not encrypt the message. - -```dns -tx202609._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." -``` - -The `p=` value above is a truncated illustrative key; publish the full base64 your signer generates. Sign with `rsa-sha256`: RFC 8301 prohibits `rsa-sha1` outright, sets a hard floor of 1024-bit RSA keys, and recommends 2048-bit or larger, so treat 2048-bit as the working minimum rather than the 1024-bit floor. Rotate by publishing a new selector's public key first, switching signing to it, and only then retiring the old selector once mail signed with it has cleared the network; an empty `p=` revokes a key. Publishing a key is not enough on its own: the sending service must actually sign, and a receiver must actually verify, before DKIM contributes to a DMARC pass. - -Unverified until checked in your deployment: whether signing is enabled, the live `d=` and selector, the key length, whether the key is a TXT record or a provider CNAME delegation, and who owns rotation. - -## 3. DMARC: align authentication to the visible From and enforce - -DMARC is the record that finally protects the `From:` your users see. A message passes DMARC when at least one of SPF or DKIM both passes and is aligned: SPF alignment compares the envelope domain, DKIM alignment compares the verified signature's `d=`, each against the visible `From:` domain. Relaxed alignment (`adkim=r`, `aspf=r`, the default) accepts the same organizational domain; strict (`s`) requires an exact match. - -```dns -_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; sp=reject; np=reject; adkim=r; aspf=r; rua=mailto:dmarc-agg@example.com" -``` - -`p=` sets the requested handling of failures: `none` asks for no change (monitor only, and still spoofable), `quarantine` asks receivers to treat failures as suspicious, and `reject` asks them to reject at SMTP time. `sp=` sets the policy for existing subdomains and `np=` for non-existent ones; where a tag is absent, policy falls back `np` to `sp` to `p`, so a `p=reject` domain already rejects for its own subdomains without them. Publish `sp` and `np` when you want subdomains held to a policy of their own, whether it repeats or differs from `p`, and inventory any subdomain that publishes its own DMARC record, since a receiver's tree walk finds that record first. DMARC discovers the applicable policy and organizational domain by a DNS tree walk (RFC 9989 section 4.10), which replaced the old public-suffix-list method. `t=y` weakens enforcement for testing and `t=n` is the default; the enforcement record above omits it. - -Reach `p=reject` only after a monitoring period under `p=none` with aggregate reports confirms every legitimate stream aligns, or you will reject your own mail. Missing DMARC or `p=none` means no enforcement is requested, not that mail is guaranteed to arrive; receivers keep their own local policy. One legacy note: RFC 9989 (May 2026), which now defines DMARC and obsoletes RFC 7489, removed the `pct` tag, so a `pct` value is not a current rollout control, and a legacy `p=reject; pct=25` was partial application rather than full enforcement. - -Unverified until checked: which streams actually align, the inherited and explicit subdomain policies, and whether your receivers implement the current specification. - -## 4. DMARC reporting: aggregate now, failure reports later - -Aggregate reporting (`rua=`, RFC 9990) sends a daily XML summary of authentication results and is how you see what is sending as your domain before you enforce; turn it on first. A destination in another domain must authorize the reports, or receivers will not send them: - -```dns -example.com._report._dmarc.collector.example.net. IN TXT "v=DMARC1" -``` - -Failure reporting (`ruf=`, RFC 9991) sends per-message forensic copies that can include message content and recipient data, which for password-reset mail can mean leaking a live reset token to whatever inbox collects the reports. Omit `ruf` at first, and enable it only after you have settled collection, redaction, retention, and access, and authorized any external destination the same way. Publication alone is not evidence reports arrive: verify the mailbox actually receives and processes them. - -## 5. SMTP submission and relay: TLS before AUTH, and never an open relay - -Where your application hands mail to a mail server, three ports carry different contracts. Port 587 is submission with STARTTLS: the client upgrades to TLS and then authenticates, but the STARTTLS offer alone does not force encryption, so an opportunistic client that falls back to plaintext is exposed to a STARTTLS-stripping downgrade that captures the credential. Port 465 is submission with implicit TLS: the connection is encrypted from the first byte and is immune to stripping, which is why RFC 8314 prefers it going forward. Require successful TLS with certificate chain and hostname validation before any credential is sent on either, and treat TLS 1.2 as the floor (RFC 8997). Port 25 is inter-server relay or, at some providers, submission; "port 25 means no authentication" is wrong (RFC 6409 section 3.1), so identify which role your endpoint plays rather than assuming. - -An open relay forwards mail from an unauthenticated, untrusted client to a destination the server is not responsible for; it will be found and abused, and your domain blocked. Reject that forwarding while still allowing the two legitimate cases: ordinary unauthenticated inbound delivery to your own local recipients, and explicitly authorized relay paths (RFC 2505 section 2.1). After TLS, an unauthenticated or invalidly authenticated submission must fail and an authorized one succeed. - -Unverified until checked: the live endpoints and ports, whether the client enforces TLS rather than merely offering it, the authentication mechanism, and the relay access rules. - -## 6. Transport and display: MTA-STS, TLS-RPT, and BIMI - -MTA-STS (RFC 8461) lets a receiving domain demand TLS for mail sent to it. A TXT record points to an HTTPS-hosted policy: - -```dns -_mta-sts.example.com. IN TXT "v=STSv1; id=2026091501" -``` - -The policy is served as `text/plain` at `https://mta-sts.example.com/.well-known/mta-sts.txt`: - -```text -version: STSv1 -mode: enforce -mx: mx1.example.com -max_age: 604800 -``` - -Only `mode: enforce` constrains delivery (`testing` and `none` do not), and the crucial caveat is direction: MTA-STS protects mail arriving at the policy domain, so publishing it does not secure your own outbound mail to other domains, and it depends on your sending server validating recipients' policies. Update the `id` whenever the policy changes. TLS-RPT (RFC 8460) is reporting only, never enforcement: - -```dns -_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-rpt@example.com" -``` - -BIMI is optional display metadata that shows a brand logo in supporting clients; it depends on DMARC enforcement already being in place and is not an authentication mechanism or a guarantee the logo renders. Keep it last, after the records above are enforcing. - -## Verify - -REASONED: following block; DNS publication, submission TLS and plaintext AUTH refusal, per the guide's cited BIND 9.18.39 dig manual, OpenSSL 3.0 s_client, RFC 8314: Cleartext Considered Obsolete and RFC 8997 section 3. No controlled DNS zone or SMTP endpoint is available, and local listeners are denied. bash -n and ShellCheck 0.11.0 exited 0 without diagnostics; openssl s_client -help exited 0 and listed -starttls, -verify_hostname, -verify_return_error and -min_protocol. Published records should match the configured policies; exposed submission accepts plaintext AUTH, while fixed submission refuses it and supports validated TLS before authorized AUTH. These live outcomes remain unobserved. Every probe below is reasoned, not demonstrated: the authoring environment has no DNS control, no SMTP endpoint, and no container runtime, so the outcomes are derived from the cited RFCs rather than observed. For the submission probes, a transport failure or a TLS error is inconclusive, never the fixed state. - -```bash -# REASONED: DNS and TLS expectations follow the cited RFCs; no DNS control, SMTP endpoint, or container runtime is available. -# Inspect the published records (replace names; take the DKIM selector from a real signature). -# These read DNS and TLS state; they do not evaluate SPF or DMARC or prove a service listener. -dig +noall +answer TXT bounce.example.com -dig +noall +answer TXT tx202609._domainkey.example.com -dig +noall +answer CNAME tx202609._domainkey.example.com -dig +noall +answer TXT _dmarc.example.com -dig +noall +answer TXT _mta-sts.example.com -dig +noall +answer TXT _smtp._tls.example.com -dig +noall +answer TXT default._bimi.example.com - -# Submission must complete TLS with a validated certificate before AUTH. -verify_return_error -# matters because s_client otherwise continues past a verification failure. This confirms TLS is -# available, not that AUTH is gated on it; the discriminating check, reasoned here, is the negative -# one: a plaintext or post-stripped-STARTTLS AUTH attempt must be refused, where the exposed state -# accepts it. -openssl s_client -connect smtp.example.com:587 -starttls smtp \ - -servername smtp.example.com -verify_hostname smtp.example.com \ - -verify_return_error -min_protocol TLSv1.2 -**Version basis** +Written against nginx documentation (no recorded version), Apache documentation 2.4, Caddy documentation v2.11.4, Next.js browser source maps v16.3.6, Vite production source maps v8.3.1, Debian apache2 packaging 2.4.69-1, ACME HTTP-01 RFC 8555, and GNU grep 3.12; 1 further source is pinned to a commit and 5 carry no recorded version. Metadata reviewed 2026-10-18; documentation checked September 2026. The guide records 26 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): - -- nginx: nginx documentation (unknown) -- apache: Apache documentation (2.4) -- caddy: Caddy documentation (v2.11.4) -- next: Next.js documentation (unknown) -- vite: Vite documentation (unknown) -- cra: Create React App documentation (unknown) -- coreutils: GNU coreutils documentation (unknown) -- grep: GNU grep (3.12) -- caddy-rolling: Caddy documentation (rolling) (unknown) -- acme-http01: ACME HTTP-01 (RFC 8555) -- debian-packaging: Debian apache2 packaging (2.4.69-1) -- ecma426: ECMA-426 (c724f0f494b44b21b651fdbd6412d3437eef0e72) -- next-maps: Next.js browser source maps (v16.3.6) -- vite-maps: Vite production source maps (v8.3.1) - -Claims (full source mapping in the front matter): - -- nginx-dotfiles (REASONED; nginx): Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them. -- nginx-backups (REASONED; nginx): Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence. -- nginx-acme (REASONED; nginx): Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets. -- nginx-security-txt (REASONED; nginx): The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations. -- apache-directories (REASONED; apache): DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/. -- apache-files (REASONED; apache): Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config. -- apache-listing (REASONED; apache, debian-packaging): Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide. -- apache-forbidden (REASONED; apache): mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed. -- apache-options (REASONED; apache): Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options. -- nginx-listing (REASONED; nginx): nginx autoindex defaults off. -- caddy-listing (REASONED; caddy-rolling, caddy): Guide says Caddy lists only with file_server browse. -- caddy-deny (REASONED; caddy, caddy-rolling): Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run. -- caddy-acme (REASONED; caddy, caddy-rolling): ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets. -- backup-storage (REASONED; nginx, apache, caddy, caddy-rolling): Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names. -- suffix-policy (REASONED; nginx, apache, caddy, caddy-rolling): Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible. -- next-public (REASONED; next): NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes. -- vite-public (REASONED; vite): VITE_ embeds values in browser JavaScript; reserve for public values. -- cra-public (REASONED; cra): Deprecated Create React App embeds REACT_APP_ values in browser builds. -- source-maps (REASONED; next, vite, ecma426, next-maps, vite-maps): ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public. -- verify-dotfiles (REASONED; nginx, apache, caddy, caddy-rolling): Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes. -- verify-listing (REASONED; apache): Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward. -- verify-acme (REASONED; nginx, apache, acme-http01): Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive. -- verify-lookalike (REASONED; nginx, apache, caddy, caddy-rolling): Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it. -- verify-bundle (REASONED; next, vite, cra, grep): Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets. -- verify-backup (REASONED; nginx, apache, caddy, caddy-rolling): Known previously deployed backup URLs should return 403 or 404 without the file contents. -- verify-suffixes (REASONED; nginx, apache, caddy, coreutils, caddy-rolling): GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution. - +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. Scanners request `/.env`, `/.git/config`, `/config.php.bak`, and `/db.sql` continuously, and any of these under a web root hands over credentials or source regardless of what the application itself authenticates. @@ -455,3 +249,235 @@ done - ECMA-426 c724f0f494b44b21b651fdbd6412d3437eef0e72 data URI linking and sourceMappingURL comments (checked October 2026): https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1294 ; https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1323 ; https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1415 - Next.js v16.3.6 production browser source maps default and opt-in serving (checked October 2026): https://github.com/vercel/next.js/blob/v16.3.6/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18 - Vite v8.3.1 build.sourcemap default and output modes (checked October 2026): https://github.com/vitejs/vite/blob/v8.3.1/docs/config/build-options.md#L145-L150 + + + +## 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/web-exposure.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: nginx documentation (unknown) +- apache: Apache documentation (2.4) +- caddy: Caddy documentation (v2.11.4) +- caddy-rolling: Caddy documentation (rolling) (unknown) +- next: Next.js documentation (unknown) +- vite: Vite documentation (unknown) +- cra: Create React App documentation (unknown) +- next-maps: Next.js browser source maps (v16.3.6) +- vite-maps: Vite production source maps (v8.3.1) +- debian-packaging: Debian apache2 packaging (2.4.69-1) +- acme-http01: ACME HTTP-01 (RFC 8555) +- ecma426: ECMA-426 (c724f0f494b44b21b651fdbd6412d3437eef0e72) +- coreutils: GNU coreutils documentation (unknown) +- grep: GNU grep (3.12) + +Claims (full source mapping in the JSON block below): + +- nginx-dotfiles (REASONED; nginx): Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them. +- nginx-backups (REASONED; nginx): Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence. +- nginx-acme (REASONED; nginx): Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets. +- nginx-security-txt (REASONED; nginx): The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations. +- apache-directories (REASONED; apache): DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/. +- apache-files (REASONED; apache): Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config. +- apache-listing (REASONED; apache, debian-packaging): Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide. +- apache-forbidden (REASONED; apache): mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed. +- apache-options (REASONED; apache): Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options. +- nginx-listing (REASONED; nginx): nginx autoindex defaults off. +- caddy-listing (REASONED; caddy-rolling, caddy): Guide says Caddy lists only with file_server browse. +- caddy-deny (REASONED; caddy, caddy-rolling): Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run. +- caddy-acme (REASONED; caddy, caddy-rolling): ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets. +- backup-storage (REASONED; nginx, apache, caddy, caddy-rolling): Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names. +- suffix-policy (REASONED; nginx, apache, caddy, caddy-rolling): Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible. +- next-public (REASONED; next): NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes. +- vite-public (REASONED; vite): VITE_ embeds values in browser JavaScript; reserve for public values. +- cra-public (REASONED; cra): Deprecated Create React App embeds REACT_APP_ values in browser builds. +- source-maps (REASONED; next, vite, ecma426, next-maps, vite-maps): ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public. +- verify-dotfiles (REASONED; nginx, apache, caddy, caddy-rolling): Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes. +- verify-listing (REASONED; apache): Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward. +- verify-acme (REASONED; nginx, apache, acme-http01): Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive. +- verify-lookalike (REASONED; nginx, apache, caddy, caddy-rolling): Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it. +- verify-bundle (REASONED; next, vite, cra, grep): Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets. +- verify-backup (REASONED; nginx, apache, caddy, caddy-rolling): Known previously deployed backup URLs should return 403 or 404 without the file contents. +- verify-suffixes (REASONED; nginx, apache, caddy, coreutils, caddy-rolling): GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution. + +````json +{ + "schema": 1, + "checked": "2026-10-18", + "documentation_checked": "2026-09", + "body_sha256": "d04f9d877201565cfdd6fe952b50f26d3d85bd16e74efce72a6ef79cebdee43b", + "components": { + "nginx": { + "name": "nginx documentation", + "basis": "unknown", + "sources": { + "s0e5162d1ce2b": "https://nginx.org/en/docs/http/ngx_http_access_module.html", + "sbd17bad53c9d": "https://nginx.org/en/docs/http/ngx_http_autoindex_module.html", + "s40bdf1af1596": "https://nginx.org/en/docs/http/ngx_http_core_module.html" + } + }, + "apache": { + "name": "Apache documentation", + "basis": "2.4", + "sources": { + "s24f06763d14a": "https://httpd.apache.org/docs/2.4/mod/core.html#directorymatch", + "se24e9bbc0443": "https://httpd.apache.org/docs/2.4/mod/core.html#filesmatch", + "s97468bf1f299": "https://httpd.apache.org/docs/2.4/mod/core.html#options", + "s7313b1b624d5": "https://httpd.apache.org/docs/2.4/mod/mod_authz_core.html", + "sef5ff46eddf7": "https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html" + } + }, + "caddy": { + "name": "Caddy documentation", + "basis": "v2.11.4", + "sources": { + "s450aa9ed710b": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/matchers.go", + "se8dd8140d6b8": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/fileserver/staticfiles.go#L140-L142", + "s86912a2959e0": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/fileserver/staticfiles.go#L371-L374", + "s128797d9a40f": "https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddyhttp/server.go#L384-L388" + } + }, + "caddy-rolling": { + "name": "Caddy documentation (rolling)", + "basis": "unknown", + "sources": { + "se9ab71afc4ba": "https://caddyserver.com/docs/caddyfile/directives/respond", + "s38b1b78ce980": "https://caddyserver.com/docs/caddyfile/matchers" + } + }, + "next": { + "name": "Next.js documentation", + "basis": "unknown", + "sources": { + "sb98bee5b61c8": "https://nextjs.org/docs/pages/guides/environment-variables" + } + }, + "vite": { + "name": "Vite documentation", + "basis": "unknown", + "sources": { + "s3fc0892e0f58": "https://vite.dev/guide/env-and-mode" + } + }, + "cra": { + "name": "Create React App documentation", + "basis": "unknown", + "sources": { + "scfb24624addd": "https://create-react-app.dev/docs/adding-custom-environment-variables/" + } + }, + "next-maps": { + "name": "Next.js browser source maps", + "basis": "v16.3.6", + "sources": { + "s6964da09fc21": "https://github.com/vercel/next.js/blob/v16.3.6/docs/01-app/03-api-reference/05-config/01-next-config-js/productionBrowserSourceMaps.mdx#L8-L18" + } + }, + "vite-maps": { + "name": "Vite production source maps", + "basis": "v8.3.1", + "sources": { + "sf12c9114bd79": "https://github.com/vitejs/vite/blob/v8.3.1/docs/config/build-options.md#L145-L150" + } + }, + "debian-packaging": { + "name": "Debian apache2 packaging", + "basis": "2.4.69-1", + "sources": { + "s62f6ed2866ab": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/changelog#L1", + "s90898e0f03b6": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/apache2.conf.in#L170-L174", + "s6f77b580cc87": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/config-dir/mods-available/dir.conf#L1", + "s7ea1fcaca182": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.postinst#L38-L52", + "sd7d93d2a9705": "https://salsa.debian.org/apache-team/apache2/-/blob/fc6e58fa5248fc058591097c94ac8fde34283519/debian/apache2.postinst#L257-L266" + } + }, + "acme-http01": { + "name": "ACME HTTP-01", + "basis": "RFC 8555", + "sources": { + "se60bdd68e41d": "https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3" + } + }, + "ecma426": { + "name": "ECMA-426", + "basis": "c724f0f494b44b21b651fdbd6412d3437eef0e72", + "sources": { + "s5de29cfb3e99": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L584-L585", + "s2af3a1c52b4a": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1294", + "sfeb03b2200f4": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1323", + "s94b6d647e4bc": "https://github.com/tc39/ecma426/blob/c724f0f494b44b21b651fdbd6412d3437eef0e72/spec.emu#L1415" + } + }, + "coreutils": { + "name": "GNU coreutils documentation", + "basis": "unknown", + "sources": { + "sabbd081d1d5a": "https://www.gnu.org/s/coreutils/manual/html_node/mktemp-invocation.html" + } + }, + "grep": { + "name": "GNU grep", + "basis": "3.12", + "sources": { + "s5170317ddea8": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n1110", + "s057470125f4c": "https://git.savannah.gnu.org/cgit/grep.git/tree/doc/grep.texi?h=v3.12#n425", + "sb18b36e247d7": "https://git.savannah.gnu.org/cgit/grep.git/tree/src/grep.c?h=v3.12#n1756", + "s0c4ddbee9e68": "https://git.savannah.gnu.org/cgit/grep.git/tree/src/grep.c?h=v3.12#n661" + } + } + }, + "claims": { + "nginx-dotfiles": {"text": "Deny dot-prefixed path segments by regex; place deny regex locations before other matching regex locations. Exact locations or a longest matching ^~ prefix can bypass them.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-backups": {"text": "Case-insensitive suffix regex denies .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz outside the ACME exemption, subject to location precedence.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-acme": {"text": "Only /.well-known/acme-challenge/ is exempted: when its ^~ location is the longest matching prefix, it skips both deny regexes. Keep the challenge directory free of secrets.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "nginx-security-txt": {"text": "The ACME-only exemption leaves /.well-known/security.txt denied; add location = /.well-known/security.txt {} to serve it, because exact matches precede regex locations.", "components": ["nginx"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596"], "status": "REASONED"}, + "apache-directories": {"text": "DirectoryMatch denies dot-prefixed filesystem segments except an exact .well-known segment; .well-known-backup is not exempt. FilesMatch suffix denial still applies inside .well-known/.", "components": ["apache"], "sources": ["apache:s24f06763d14a", "apache:s7313b1b624d5", "apache:se24e9bbc0443"], "status": "REASONED"}, + "apache-files": {"text": "Case-insensitive FilesMatch denies dotfile basenames and .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes; DirectoryMatch is still needed for .git/config.", "components": ["apache"], "sources": ["apache:se24e9bbc0443", "apache:s7313b1b624d5"], "status": "REASONED"}, + "apache-listing": {"text": "Debian apache2 2.4.69-1 sets Indexes FollowSymLinks on /var/www/; DirectoryIndex lists index.html index.cgi index.pl index.php index.xhtml index.htm; dir and autoindex are enabled on fresh installs. With these defaults, index-less directories can expose other filenames. Ubuntu packaging was not checked by this guide.", "components": ["apache", "debian-packaging"], "sources": ["apache:s97468bf1f299", "apache:sef5ff46eddf7", "debian-packaging:s62f6ed2866ab", "debian-packaging:s90898e0f03b6", "debian-packaging:s6f77b580cc87", "debian-packaging:s7ea1fcaca182", "debian-packaging:sd7d93d2a9705"], "status": "REASONED"}, + "apache-forbidden": {"text": "mod_autoindex hides entries whose subrequests return 403 unless ShowForbidden is enabled; unmatched exports such as archive.zip and customers.csv can still be listed.", "components": ["apache"], "sources": ["apache:sef5ff46eddf7"], "status": "REASONED"}, + "apache-options": {"text": "Options -Indexes removes listing, preserving inherited options; Apache 2.4 rejects mixing relative +/- and bare options.", "components": ["apache"], "sources": ["apache:s97468bf1f299"], "status": "REASONED"}, + "nginx-listing": {"text": "nginx autoindex defaults off.", "components": ["nginx"], "sources": ["nginx:sbd17bad53c9d"], "status": "REASONED"}, + "caddy-listing": {"text": "Guide says Caddy lists only with file_server browse.", "components": ["caddy-rolling", "caddy"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:se8dd8140d6b8", "caddy:s86912a2959e0"], "status": "REASONED"}, + "caddy-deny": {"text": "Caddy v2.11.4 matchers return 404 for dot-prefixed segments at any depth outside root /.well-known/*, and case-insensitive .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz suffixes everywhere. Server execution was not run.", "components": ["caddy", "caddy-rolling"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "caddy-acme": {"text": "ANDed path_regexp and not path matchers exempt the root /.well-known/ subtree only from dotfile denial; suffix denial still applies and .well-known-backup is not exempt. Keep the subtree free of secrets.", "components": ["caddy", "caddy-rolling"], "sources": ["caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b", "caddy:s128797d9a40f"], "status": "REASONED"}, + "backup-storage": {"text": "Keep dumps and backups outside the document root or in object storage; filename denies are a backstop and cannot recognize secrets or backups with unmatched names.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "suffix-policy": {"text": "Suffix denies also block intended public downloads, including release archives; narrow the policy for the site and verify intended downloads remain accessible.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "next-public": {"text": "NEXT_PUBLIC_ embeds values in browser JavaScript; keep provider keys server-side behind backend routes.", "components": ["next"], "sources": ["next:sb98bee5b61c8"], "status": "REASONED"}, + "vite-public": {"text": "VITE_ embeds values in browser JavaScript; reserve for public values.", "components": ["vite"], "sources": ["vite:s3fc0892e0f58"], "status": "REASONED"}, + "cra-public": {"text": "Deprecated Create React App embeds REACT_APP_ values in browser builds.", "components": ["cra"], "sources": ["cra:scfb24624addd"], "status": "REASONED"}, + "source-maps": {"text": "ECMA-426 sources are URLs or null; sourcesContent is optional. Guide assessment: maps can expose original source when embedded or retrievable, through separate files or sourceMappingURL data: URLs. Next.js v16.3.6 production browser maps default disabled; Vite v8.3.1 build.sourcemap defaults false. Create React App map defaults were not checked. Guide reasoning: removing the comment does not unpublish deployed maps. Guide advice: do not ship or leave served maps for code not meant to be public.", "components": ["next", "vite", "ecma426", "next-maps", "vite-maps"], "sources": ["next:sb98bee5b61c8", "vite:s3fc0892e0f58", "ecma426:s5de29cfb3e99", "ecma426:s2af3a1c52b4a", "ecma426:sfeb03b2200f4", "ecma426:s94b6d647e4bc", "next-maps:s6964da09fc21", "vite-maps:sf12c9114bd79"], "status": "REASONED"}, + "verify-dotfiles": {"text": "Plant an exclusive, readable .env.probe file; expect 403 or 404 without PLANTED-SECRET. Also probe .git/config, config.php.bak and db.sql; absent files are inconclusive, so plant real files to test each rule and remove only created probes.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:s24f06763d14a", "apache:s7313b1b624d5", "apache:se24e9bbc0443", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED", "verify": [1]}, + "verify-listing": {"text": "Plant a throwaway file in an existing index-less uploads directory; Options -Indexes should yield 403 or 404, never a 200 listing. An absent or unwritable directory is inconclusive; remove the probe afterward.", "components": ["apache"], "sources": ["apache:s97468bf1f299", "apache:sef5ff46eddf7"], "status": "REASONED", "verify": [1]}, + "verify-acme": {"text": "Plant a readable challenge token; webroot requests over HTTPS and HTTP:80 should return probe, not 403. nginx exempts only the challenge prefix; Apache exempts .well-known segments. Failed preparation is inconclusive.", "components": ["nginx", "apache", "acme-http01"], "sources": ["nginx:s40bdf1af1596", "nginx:s0e5162d1ce2b", "apache:s24f06763d14a", "acme-http01:se60bdd68e41d"], "status": "REASONED", "verify": [1]}, + "verify-lookalike": {"text": "Probe .well-known-backup/config across nginx, Apache and Caddy; expect 403 or 404. Plant a real readable file to distinguish denial from absence, then remove it.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:s24f06763d14a", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED", "verify": [1]}, + "verify-bundle": {"text": "Scan built client output for credential patterns/literal secrets; GNU grep 3.12 returns 0 for a selected line, 1 for none and 2 for an error. A missing directory is an error; discarding errors can leave silence without a clean result. Clean does not exclude transformed secrets.", "components": ["next", "vite", "cra", "grep"], "sources": ["next:sb98bee5b61c8", "vite:s3fc0892e0f58", "cra:scfb24624addd", "grep:s5170317ddea8", "grep:s057470125f4c", "grep:sb18b36e247d7", "grep:s0c4ddbee9e68"], "status": "REASONED", "verify": [1]}, + "verify-backup": {"text": "Known previously deployed backup URLs should return 403 or 404 without the file contents.", "components": ["nginx", "apache", "caddy", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b"], "status": "REASONED"}, + "verify-suffixes": {"text": "GNU mktemp --suffix plants readable files for .bak, .old, .orig, ~, .sql, .sql.gz, .dump and .tar.gz plus a generic dotfile. Exposed: 200 with PLANTED-SECRET; fixed: 403 or 404 without it. Failed planting or curl is inconclusive; only local shell syntax and suffix creation were checked, not server execution.", "components": ["nginx", "apache", "caddy", "coreutils", "caddy-rolling"], "sources": ["nginx:s0e5162d1ce2b", "nginx:s40bdf1af1596", "apache:se24e9bbc0443", "apache:s7313b1b624d5", "caddy-rolling:se9ab71afc4ba", "caddy-rolling:s38b1b78ce980", "caddy:s450aa9ed710b", "coreutils:sabbd081d1d5a"], "status": "REASONED", "verify": [2]} + } +} +```` +