diff --git a/scripts/test-verify-workspace-install-docs.sh b/scripts/test-verify-workspace-install-docs.sh index c1546d1b..92e96cd0 100755 --- a/scripts/test-verify-workspace-install-docs.sh +++ b/scripts/test-verify-workspace-install-docs.sh @@ -245,6 +245,27 @@ switch (mutation) { case "nginx-additional-bypass": changed = original.replace(" location / {", " location /bypass {\n proxy_pass http://127.0.0.1:8080;\n }\n\n location / {"); break; + case "nginx-comment-only-auth": + changed = original.replace(" location / {", ` location /comment-only-auth { + # auth_request /_authenticate; + # auth_request_set $thoth_principal_issuer $upstream_http_x_thoth_principal_issuer; + # auth_request_set $thoth_principal_subject $upstream_http_x_thoth_principal_subject; + # auth_request_set $thoth_principal_display_name $upstream_http_x_thoth_principal_display_name; + # auth_request_set $thoth_is_admin $upstream_http_x_thoth_is_admin; + # proxy_set_header X-Thoth-Principal-Issuer ""; + # proxy_set_header X-Thoth-Principal-Subject ""; + # proxy_set_header X-Thoth-Principal-Display-Name ""; + # proxy_set_header X-Thoth-Is-Admin ""; + # proxy_set_header X-Thoth-Trusted-Principal-Issuer $thoth_principal_issuer; + # proxy_set_header X-Thoth-Trusted-Principal-Subject $thoth_principal_subject; + # proxy_set_header X-Thoth-Trusted-Principal-Display-Name $thoth_principal_display_name; + # proxy_set_header X-Thoth-Trusted-Is-Admin $thoth_is_admin; + proxy_set_header X-Comment-Literal "quoted#value"; # preserve the quoted hash + proxy_pass http://127.0.0.1:8080; # active frontend path + } + + location / {`); + break; case "caddy-no-auth": changed = original.replace("forward_auth auth-gateway:4180 {", "# forward authentication omitted"); break; @@ -412,6 +433,10 @@ expect_guide_rejected \ "Nginx additional frontend bypass location" verify_reverse_proxy_nginx_guide \ "$root/docs/install/reverse-proxy-nginx.md" docs/install/reverse-proxy-nginx.md nginx-additional-bypass \ "Nginx frontend upstream location bypasses complete authentication contract" +expect_guide_rejected \ + "Nginx frontend auth directives only in comments" verify_reverse_proxy_nginx_guide \ + "$root/docs/install/reverse-proxy-nginx.md" docs/install/reverse-proxy-nginx.md nginx-comment-only-auth \ + "Nginx frontend upstream location bypasses complete authentication contract" expect_guide_rejected \ "Caddy identity without authentication" verify_reverse_proxy_caddy_guide \ "$root/docs/install/reverse-proxy-caddy.md" docs/install/reverse-proxy-caddy.md caddy-no-auth \ diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh index e38576aa..35435519 100755 --- a/scripts/verify-workspace-install-docs.sh +++ b/scripts/verify-workspace-install-docs.sh @@ -639,6 +639,48 @@ verify_reverse_proxy_nginx_guide() { const fs = require("fs"); const source = fs.readFileSync(process.argv[2], "utf8"); const block = [...source.matchAll(/```nginx\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n"); +function stripNginxComments(text) { + let effective = ""; + let quote = null; + let escaped = false; + let comment = false; + for (const character of text) { + if (comment) { + if (character === "\n") { + effective += character; + comment = false; + } + continue; + } + if (escaped) { + effective += character; + escaped = false; + continue; + } + if (character === "\\") { + effective += character; + escaped = true; + continue; + } + if (quote !== null) { + effective += character; + if (character === quote) quote = null; + continue; + } + if (character === '"' || character === "'") { + effective += character; + quote = character; + continue; + } + if (character === "#") { + comment = true; + continue; + } + effective += character; + } + return effective; +} +const effectiveBlock = stripNginxComments(block); const tokens = [ "listen 443 ssl;", "ssl_certificate ", "ssl_certificate_key ", "location = /_authenticate {", "internal;", "proxy_pass http://auth-gateway:4180/verify;", @@ -646,13 +688,13 @@ const tokens = [ "proxy_http_version 1.1;", "proxy_buffering off;", "proxy_cache off;", "proxy_read_timeout 3600s;", ]; -if (/127\.0\.0\.1:8787|\bcore:8787\b/.test(block) || !block.includes("http://127.0.0.1:8080")) { +if (/127\.0\.0\.1:8787|\bcore:8787\b/.test(effectiveBlock) || !effectiveBlock.includes("http://127.0.0.1:8080")) { throw new Error("Nginx proxy must forward only to frontend on 127.0.0.1:8080"); } for (const token of tokens) { - if (!block.includes(token)) throw new Error(`Nginx proxy lacks structural token: ${token}`); + if (!effectiveBlock.includes(token)) throw new Error(`Nginx proxy lacks structural token: ${token}`); } -if (/proxy_set_header\s+X-Thoth-Trusted-[^;]+\$http_/i.test(block)) { +if (/proxy_set_header\s+X-Thoth-Trusted-[^;]+\$http_/i.test(effectiveBlock)) { throw new Error("Nginx proxy trusts a client-supplied identity header"); } const identities = [ @@ -681,7 +723,7 @@ function nginxLocations(text) { } return locations; } -const locations = nginxLocations(block); +const locations = nginxLocations(effectiveBlock); const authLocations = locations.filter((location) => location.selector === "= /_authenticate"); if (authLocations.length !== 1) { throw new Error("Nginx proxy must define exactly one authentication location");