From fc64cc95afe26a8222ced318977565c3a4d27d52 Mon Sep 17 00:00:00 2001 From: Hannes Zietsman Date: Mon, 2 Nov 2026 17:46:06 +0200 Subject: [PATCH] fix(fleet): adopt the released 1.6 standard configuration --- .traceweaver/intent-contract.yml | 55 +- requirements.md | 15 + src/cryptolabs_proxy/standard_adoption.py | 17 +- .../previous-standard-nginx-8e2b421.conf.j2 | 347 +++++++++++ tests/fixtures/main16-letsencrypt.conf | 567 ++++++++++++++++++ tests/fixtures/main16-self-signed.conf | 551 +++++++++++++++++ tests/test_standard_adoption.py | 67 ++- traceability-matrix.md | 23 + 8 files changed, 1613 insertions(+), 29 deletions(-) create mode 100644 src/cryptolabs_proxy/templates/previous-standard-nginx-8e2b421.conf.j2 create mode 100644 tests/fixtures/main16-letsencrypt.conf create mode 100644 tests/fixtures/main16-self-signed.conf diff --git a/.traceweaver/intent-contract.yml b/.traceweaver/intent-contract.yml index c0b8f71..2464348 100644 --- a/.traceweaver/intent-contract.yml +++ b/.traceweaver/intent-contract.yml @@ -69,22 +69,41 @@ "verification_id": "VER-PAIR-006", "validation_id": "VAL-PAIR-006" } - ] + ], + "service_recovery_amendment_projection": { + "date": "2026-11-02", + "authority_plan": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/architecture/continuation-amendment.json", + "plan_sha256": "159af8421e4dde61e4ecc4f47849a1ddd7f07cd6fb158d247256f0bb36ed316e", + "owner_approval": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/owner-approval.json", + "implementation_contract_sha256": "91b873c548da7d10a113c8fbb74f09540559ba51339514c738e081d0dc75cf01", + "authority_gate": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/go/recovery-source-go.json", + "reviewed_vv": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/go/recovery-vv-root-reviewed.json", + "requirement": "REQ-FLEETUPDATE-001", + "trace": "TRACE-FLEETUPDATE-001", + "verification": "VER-FLEETUPDATE-001", + "validation": "VAL-FLEETUPDATE-001", + "source_only": true, + "native_acceptance": "held", + "production_writes": "disabled", + "publication": "held" + }, + "main16_compatibility": { + "work_item_id": "FLEET-MAIN16-COMPAT-20261102", + "requirement": "REQ-FLEETUPDATE-001", + "trace": "TRACE-FLEETUPDATE-001", + "verification": "VER-FLEETUPDATE-001", + "validation": "VAL-FLEETUPDATE-001", + "approved_amendment_sha256": "159af8421e4dde61e4ecc4f47849a1ddd7f07cd6fb158d247256f0bb36ed316e", + "accepted_plan": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/plan.json", + "root_source_go": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/root-source-go.json", + "frozen_profile_sha256": "dd1692f7a21e9d9331049970d7176af59cf9cc7ee9a3f3fb975c227b08424a5d", + "released_main_source": "8e2b421b65f6d8d47f780e5681d0f3d690686a6d", + "stock_profile": "standard-8e2b421", + "evidence_directory": "../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat", + "frozen_data_exception": "Exact copied Main template and independently rendered static inputs retain original bytes; external matrix SHA256 mapping replaces new inline anchors.", + "must_not_change": "auth/current target/lifecycle/dependencies/versions/customer settings/publication/native authority", + "acceptance": "Local candidate only; final independent review, protected CI/native/installed browser/site acceptance held", + "prior_recovery_budget_reset": false, + "new_native_budget": 0 + } } - -service_recovery_amendment_projection: - date: 2026-11-02 - authority_plan: ../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/architecture/continuation-amendment.json - plan_sha256: 159af8421e4dde61e4ecc4f47849a1ddd7f07cd6fb158d247256f0bb36ed316e - owner_approval: ../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/owner-approval.json - implementation_contract_sha256: 91b873c548da7d10a113c8fbb74f09540559ba51339514c738e081d0dc75cf01 - authority_gate: ../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/go/recovery-source-go.json - reviewed_vv: ../../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/go/recovery-vv-root-reviewed.json - requirement: REQ-FLEETUPDATE-001 - trace: TRACE-FLEETUPDATE-001 - verification: VER-FLEETUPDATE-001 - validation: VAL-FLEETUPDATE-001 - source_only: true - native_acceptance: held - production_writes: disabled - publication: held diff --git a/requirements.md b/requirements.md index 3151841..d5f9171 100644 --- a/requirements.md +++ b/requirements.md @@ -21,6 +21,21 @@ verified. No feature version bump or publication is part of this worktree change # CryptoLabs Proxy scoped requirements +## Main 1.6 stock-profile compatibility evidence + +`FLEET-MAIN16-COMPAT-20261102` is a bounded integration repair under the existing +REQ-FLEETUPDATE-001 authority. The owner-approved amendment SHA256 is +`159af8421e4dde61e4ecc4f47849a1ddd7f07cd6fb158d247256f0bb36ed316e`. +The accepted [child plan](../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/plan.json) +and [root source GO](../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/root-source-go.json) +bound this repair to the exact released Main `8e2b421` standard profile. Existing +Fleet and pairing requirement statements remain authoritative. Verification +uses VER-FLEETUPDATE-001 and the [worktree RED and V&V definition](../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/definition/). +Local implementation, package evidence and final independent review do not close +VAL-FLEETUPDATE-001; protected CI, native and installed browser/site acceptance +remain separately held. No recovery or native operation budget is reset. + + ## Owner-approved account pairing — PAIR-20261028-01 Source: latest owner approval quoted in `.traceweaver/account-pairing-20261028/authority.json`. Reviewed scope hash: `9b80ccd96837cae1dd63723fa10ce8390dc4305fcc9e65f303a4868a917dc922`. The exact shared protocol is `.traceweaver/account-pairing-20261028/contract.json`. Existing requirements remain authoritative; this appendix governs only the approved pairing increment. Delivery and paid-service acceptance remain held until observed. diff --git a/src/cryptolabs_proxy/standard_adoption.py b/src/cryptolabs_proxy/standard_adoption.py index 32d80de..e2f821c 100644 --- a/src/cryptolabs_proxy/standard_adoption.py +++ b/src/cryptolabs_proxy/standard_adoption.py @@ -230,15 +230,25 @@ def render_standard(declaration): PREVIOUS_STANDARD_SOURCE = '444d7c0e06e0f9935971c51872e9f37bd8869282' +MAIN16_STANDARD_SOURCE = '8e2b421b65f6d8d47f780e5681d0f3d690686a6d' PREVIOUS_STANDARD_TEMPLATES = {'previous-standard-nginx-444d7c0.conf.j2': 'f12e2b9800cbaa0e49e685506cfaad38eb7e568fa6d239fa15277af59073d8de', 'vast-price-manager-auth-blocks.conf.j2': 'b9d7013434557492887b0600bc2afc92227ee17f9efebe55b0cccc4b16bd3f81', 'acme-challenge.conf.j2': '7e594162700825757c9d402971094448aa1834d1f7121ac1c02f152f345e149a', 'fleet-api-routes.conf.j2': '6991ca6bf1dd4b258e8ee7851fc69ef50b19dfe2353570e4911a0ebadc68ed1b', 'vast-price-manager.conf.j2': '32483d169a09c42653396e66c5c7efe1995bd599f44f11ee64fa2f0731a0a2f8', 'standard-service-routes.conf.j2': 'ef0f55aae48e35127d288781e68281a371505ba27384c8fd04dff9200dce3963', 'exporter-auth-routes.conf.j2': '450f4c418c09474ac45b6dd314c47cc090fe7d230a0206b30e48c7975023133b', 'host-forwarding.conf.j2': '35c4a1fe1d3001865a77d7653350b7a61968953b94172dc7063bc31a42c0f9f7'} -def render_previous_standard(declaration): +# TRACEWEAVER: entrypoint=render_previous_standard; req=REQ-FLEETUPDATE-001; trace=TRACE-FLEETUPDATE-001; ver=VER-FLEETUPDATE-001 +def render_previous_standard(declaration, *, source=PREVIOUS_STANDARD_SOURCE): + template='previous-standard-nginx-444d7c0.conf.j2' + templates=PREVIOUS_STANDARD_TEMPLATES + if source==MAIN16_STANDARD_SOURCE: + templates={name:digest for name,digest in templates.items() if name!=template} + template='previous-standard-nginx-8e2b421.conf.j2' + templates[template]='05828b78143c5d6cd7cbe60700cfc54b627cb4efff9a7af43439fdb56e25edab' + elif source!=PREVIOUS_STANDARD_SOURCE: + raise AdoptionError('previous_standard_profile_unavailable') env=get_jinja_env() - for name,expected in PREVIOUS_STANDARD_TEMPLATES.items(): + for name,expected in templates.items(): raw=env.loader.get_source(env,name)[0].encode() if sha256(raw)!=expected: raise AdoptionError('previous_standard_profile_unavailable') - return env.get_template('previous-standard-nginx-444d7c0.conf.j2').render( + return env.get_template(template).render( domain=declaration['domain'],letsencrypt=declaration['tls']=='letsencrypt',services=declaration['services'], host_forward_groups=declaration.get('host_forward_groups',[]),acme_challenge=declaration.get('acme_challenge'), hostname_policy=validate_hostname_policy(declaration.get('hostname_policy'),declaration['domain']), @@ -640,6 +650,7 @@ def visit(items): actual=normalize(tree); expected=normalize(parse_nginx(render_standard(declaration))) if actual==expected: return 'standard' if actual==normalize(parse_nginx(render_previous_standard(declaration))): return 'standard-444d7c0' + if actual==normalize(parse_nginx(render_previous_standard(declaration,source=MAIN16_STANDARD_SOURCE))): return 'standard-8e2b421' legacy=get_jinja_env().loader.get_source(get_jinja_env(),'legacy-bootstrap-nginx.conf')[0].encode() profiles=[legacy] for enabled in (False,True): profiles.append(render_managed_vpm_config(legacy,enabled)) diff --git a/src/cryptolabs_proxy/templates/previous-standard-nginx-8e2b421.conf.j2 b/src/cryptolabs_proxy/templates/previous-standard-nginx-8e2b421.conf.j2 new file mode 100644 index 0000000..d1dd2d8 --- /dev/null +++ b/src/cryptolabs_proxy/templates/previous-standard-nginx-8e2b421.conf.j2 @@ -0,0 +1,347 @@ +# TRACEWEAVER: file-role=pairing-routing; req=REQ-PAIR-001; trace=TRACE-PAIR-001; ver=VER-PAIR-001 +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + client_max_body_size 100M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + + {% if letsencrypt or http_policy %} + # Redirect HTTP to HTTPS + server { + listen 80{% if hostname_policy %} default_server{% endif %}; + server_name {{ domain }}; +{% if hostname_policy %} + if ($host != {{ domain }}) { + return 444; + } +{% endif %} + +{% if http_policy %} + location ^~ /.well-known/acme-challenge/ { + root /var/www/certbot; + default_type text/plain; + try_files $uri =404; + } + location / { + return 308 https://{{ domain }}$request_uri; + } + {% include "vast-price-manager-auth-blocks.conf.j2" %} +{% else %} + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +{% endif %} + } + + server { + listen 443 ssl http2{% if hostname_policy %} default_server{% endif %}; + server_name {{ domain }}; +{% if hostname_policy %} + if ($host != {{ domain }}) { + return 444; + } +{% endif %} + + {% if letsencrypt %} + ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + {% else %} + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + {% endif %} + {% else %} + server { + listen 80{% if hostname_policy %} default_server{% endif %}; + listen 443 ssl http2{% if hostname_policy %} default_server{% endif %}; + server_name {{ domain }}; +{% if hostname_policy %} + if ($host != {{ domain }}) { + return 444; + } +{% endif %} + + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + {% endif %} + + # DNS resolver for Docker networks - CRITICAL for runtime resolution + # This allows nginx to start even if backend services don't exist yet + resolver 127.0.0.11 valid=10s ipv6=off; + resolver_timeout 5s; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + +{% include 'acme-challenge.conf.j2' %} + # ===================================================== + # ERROR HANDLERS + # ===================================================== + + location @service_unavailable { + default_type application/json; + return 503 '{"error":"Service unavailable","message":"The requested service is not running. Install it or wait for it to start."}'; + } + + # ===================================================== + # AUTHENTICATION + # ===================================================== + + # Auth server endpoints (public) + location ^~ /api/account-link/ { + proxy_pass http://127.0.0.1:8081; +{% if http_policy %} + proxy_hide_header X-Mgmt-Token; + proxy_cookie_flags fleet_session secure httponly samesite=lax; +{% endif %} + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Cookie $http_cookie; + } + + location /auth/ { + proxy_pass http://127.0.0.1:8081/auth/; +{% if http_policy %} + proxy_hide_header X-Mgmt-Token; + proxy_cookie_flags fleet_session secure httponly samesite=lax; +{% endif %} + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + +{% if http_policy %} + location = /auth/headers { return 404; } + location = /auth/headers/ { return 404; } + location = /auth/exporters/authorize { return 404; } + location = /auth/exporters/authorize/ { return 404; } +{% endif %} + # Internal auth verification endpoint (for auth_request) + location = /_auth_check { + internal; + proxy_pass http://127.0.0.1:8081/auth/headers; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Cookie $http_cookie; + } + + # ===================================================== + # Internal API (blocked from public access) + # Only reachable via direct Docker network (172.30.0.10:8080) + # ===================================================== + location /internal/ { + return 403; + } + + # ===================================================== + # PUBLIC ENDPOINTS (no auth required) + # ===================================================== + + # Health check API (always public) + location /api/health { + default_type application/json; + return 200 '{"status":"ok","proxy":"running"}'; + } + + # Service health check (internal, no auth) + location /api/services { + proxy_pass http://127.0.0.1:8080/api/services; + proxy_set_header Host $host; + } + + {% include "fleet-api-routes.conf.j2" %} + + # Static assets (no auth) + location /static/ { + root /usr/share/nginx/html; + } + + # ===================================================== + # PROTECTED ENDPOINTS (auth required) + # ===================================================== + + # Fleet Management Landing Page (requires auth) + location = / { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + + # Redirect to login if not authenticated + error_page 401 = @login_redirect; + + root /usr/share/nginx/html; + index index.html; + } + + location @login_redirect { + return 302 /auth/login?next=$request_uri; + } + + {% include "vast-price-manager-auth-blocks.conf.j2" %} + + {% if 'vast-price-manager' in services %} + {% include "vast-price-manager.conf.j2" %} + {% endif %} + + {% include "standard-service-routes.conf.j2" %} + + {% for name, service in services.items() %} + {% if name in ['ipmi-monitor','dc-overview','grafana','prometheus','vastai-exporter','runpod-exporter','vast-price-manager'] %} + # Vast Price Manager uses its dedicated admin-only route above. + {% elif not service.path %} + # {{ service.get('display_name', name) }} - external service, no local proxy route + {% else %} + # {{ service.get('display_name', name) }} - uses variable for runtime DNS resolution + location ~ ^{{ service.path }}(?.*)$ { + # Require authentication + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + + # Redirect to login if not authenticated + error_page 401 = @login_redirect; + + {% if name == 'grafana' or service.container_name == 'grafana' %} + # Map Fleet roles to Grafana roles (Admin, Editor, Viewer) + set $grafana_role "Viewer"; + if ($auth_role = "admin") { + set $grafana_role "Admin"; + } + if ($auth_role = "readwrite") { + set $grafana_role "Editor"; + } + {% endif %} + + # Variable allows nginx to start even if service doesn't exist yet + set $upstream_{{ name | replace('-', '_') }} http://{{ service.container_name }}:{{ service.port }}; + proxy_pass $upstream_{{ name | replace('-', '_') }}/$custom_{{ name | replace('-', '_') }}$is_args$args; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name {{ service.path.rstrip('/') }}; + + # Forward authentication headers to backend services + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated "true"; + + {% if name == 'grafana' or service.container_name == 'grafana' %} + # Grafana-specific: pass user and role for auto-login with correct permissions + proxy_set_header X-WEBAUTH-USER $auth_user; + proxy_set_header X-WEBAUTH-ROLE $grafana_role; + {% endif %} + + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_send_timeout 300; + + {% if name == 'grafana' or service.container_name == 'grafana' %} + # Inject Fleet Management Home button into Grafana HTML + # target="_top" prevents Grafana's SPA router from intercepting the click + sub_filter '' ''; + sub_filter '' '
Home|Grafana
'; + sub_filter_once on; + sub_filter_types text/html; + proxy_set_header Accept-Encoding ""; + {% elif name == 'prometheus' or service.container_name == 'prometheus' %} + # Inject Fleet Management Home button into Prometheus HTML + sub_filter '' ''; + sub_filter '' '
Home|Prometheus
'; + sub_filter_once on; + sub_filter_types text/html; + proxy_set_header Accept-Encoding ""; + {% endif %} + + # Graceful error handling when service is unavailable + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + {% endif %} + {% endfor %} + + # Exporter management pages - served by auth server + location /vastai/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + proxy_pass http://127.0.0.1:8081/vastai/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /runpod/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + proxy_pass http://127.0.0.1:8081/runpod/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + {% include "exporter-auth-routes.conf.j2" %} + + # Default: show landing page + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + } + {% include "host-forwarding.conf.j2" %} + +} diff --git a/tests/fixtures/main16-letsencrypt.conf b/tests/fixtures/main16-letsencrypt.conf new file mode 100644 index 0000000..ff7a2a8 --- /dev/null +++ b/tests/fixtures/main16-letsencrypt.conf @@ -0,0 +1,567 @@ +# TRACEWEAVER: file-role=pairing-routing; req=REQ-PAIR-001; trace=TRACE-PAIR-001; ver=VER-PAIR-001 +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + client_max_body_size 100M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + + # Redirect HTTP to HTTPS + server { + listen 80; + server_name fleet.example.test; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl http2; + server_name fleet.example.test; + + ssl_certificate /etc/letsencrypt/live/fleet.example.test/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/fleet.example.test/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + + # DNS resolver for Docker networks - CRITICAL for runtime resolution + # This allows nginx to start even if backend services don't exist yet + resolver 127.0.0.11 valid=10s ipv6=off; + resolver_timeout 5s; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # ===================================================== + # ERROR HANDLERS + # ===================================================== + + location @service_unavailable { + default_type application/json; + return 503 '{"error":"Service unavailable","message":"The requested service is not running. Install it or wait for it to start."}'; + } + + # ===================================================== + # AUTHENTICATION + # ===================================================== + + # Auth server endpoints (public) + location ^~ /api/account-link/ { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Cookie $http_cookie; + } + + location /auth/ { + proxy_pass http://127.0.0.1:8081/auth/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Internal auth verification endpoint (for auth_request) + location = /_auth_check { + internal; + proxy_pass http://127.0.0.1:8081/auth/headers; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Cookie $http_cookie; + } + + # ===================================================== + # Internal API (blocked from public access) + # Only reachable via direct Docker network (172.30.0.10:8080) + # ===================================================== + location /internal/ { + return 403; + } + + # ===================================================== + # PUBLIC ENDPOINTS (no auth required) + # ===================================================== + + # Health check API (always public) + location /api/health { + default_type application/json; + return 200 '{"status":"ok","proxy":"running"}'; + } + + # Service health check (internal, no auth) + location /api/services { + proxy_pass http://127.0.0.1:8080/api/services; + proxy_set_header Host $host; + } + + location /api/build-info { + proxy_pass http://127.0.0.1:8080/api/build-info; + } + + # Version management and update endpoints (protected) + location /api/versions { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/versions; + proxy_set_header Host $host; + proxy_connect_timeout 10s; + proxy_read_timeout 30s; + } + + location /api/update-settings { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/update-settings; + proxy_set_header Host $host; + proxy_set_header Content-Type $http_content_type; + proxy_connect_timeout 5s; + proxy_read_timeout 10s; + } + + location /api/update { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/update; + proxy_set_header Host $host; + proxy_set_header Content-Type $http_content_type; + proxy_connect_timeout 10s; + proxy_read_timeout 120s; # Updates can take time + } + + location /api/pull { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/pull; + proxy_set_header Host $host; + proxy_set_header Content-Type $http_content_type; + proxy_connect_timeout 10s; + proxy_read_timeout 300s; # Image pulls can be slow + } + + location @api_unauthorized { + default_type application/json; + return 401 '{"error":"Authentication required"}'; + } + location /assets/ { + alias /usr/share/nginx/html/assets/; + expires 1h; + } + # Static assets (no auth) + location /static/ { + root /usr/share/nginx/html; + } + + # ===================================================== + # PROTECTED ENDPOINTS (auth required) + # ===================================================== + + # Fleet Management Landing Page (requires auth) + location = / { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + + # Redirect to login if not authenticated + error_page 401 = @login_redirect; + + root /usr/share/nginx/html; + index index.html; + } + + location @login_redirect { + return 302 /auth/login?next=$request_uri; + } + + # VPM calls these endpoints only over the Docker network on port 8081. + # Exact locations keep the generic public /auth/ forwarding route from + # exposing the session authority or password reauthentication API. + location = /auth/vast-price-manager/session { return 404; } + location = /auth/vast-price-manager/session/ { return 404; } + location = /auth/vast-price-manager/reauth { return 404; } + location = /auth/vast-price-manager/reauth/ { return 404; } + # Vast Price Manager has a dedicated admin-only route. It strips the + # prefix once and explicitly preserves the remainder and query string. + location = /_vast_pricing_admin { + internal; + proxy_pass http://127.0.0.1:8081/auth/vast-price-manager/authorize; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Original-URI $request_uri; + } + + location = /vast-pricing { + return 308 /vast-pricing/; + } + + location ~ ^/vast-pricing(?/.*)$ { + auth_request /_vast_pricing_admin; + error_page 401 = @login_redirect; + + # Runtime Docker DNS resolution permits startup before this opt-in + # service has been installed. + set $upstream_vast_price_manager vast-price-manager; + proxy_pass http://$upstream_vast_price_manager:8088$vpm_upstream_path$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + # VPM directly introspects the signed Fleet session on its + # configured internal origin. Never pass client identity headers. + proxy_set_header X-Fleet-Auth-User ""; + proxy_set_header X-Fleet-Auth-Role ""; + proxy_set_header X-Fleet-Auth-Token ""; + proxy_set_header X-Fleet-Authenticated ""; + # Preserve the browser Fleet cookie for VPM's direct introspection. + proxy_set_header Cookie $http_cookie; + # Auth-request failures still use the redirect above. Do not + # intercept VPM's own 401 reauthentication result, which must + # reach its caller as an error rather than a Fleet login redirect. + proxy_intercept_errors off; + error_page 502 503 504 = @service_unavailable; + } + # IPMI Monitor API - returns JSON 401 for unauthenticated requests + location ~ ^/ipmi/api/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @api_unauthorized; + + set $upstream_ipmi http://ipmi-monitor:5000; + proxy_pass $upstream_ipmi/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /ipmi; + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # IPMI Monitor - regex location to properly rewrite URI with variable proxy_pass + location ~ ^/ipmi/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @login_redirect; + + # Variable allows nginx to start even if service doesn't exist + set $upstream_ipmi http://ipmi-monitor:5000; + proxy_pass $upstream_ipmi/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /ipmi; + # Forward authentication headers from proxy auth + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # DC Overview API - returns JSON 401 for unauthenticated requests + location ~ ^/dc/api/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @api_unauthorized; + + set $upstream_dc http://dc-overview:5001; + proxy_pass $upstream_dc/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /dc; + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # DC Overview / Server Manager - regex location to properly rewrite URI with variable proxy_pass + location ~ ^/dc/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @login_redirect; + + # Variable allows nginx to start even if service doesn't exist + set $upstream_dc http://dc-overview:5001; + proxy_pass $upstream_dc/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /dc; + # Forward authentication headers from proxy auth + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # Shortcuts for /servers and /settings -> /dc/... + location = /servers { + return 301 /dc/servers; + } + location = /settings { + return 301 /dc/settings; + } + + # Grafana - uses variable for runtime DNS resolution + location /grafana/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + + # Map Fleet roles to Grafana roles + set $grafana_role "Viewer"; + if ($auth_role = "admin") { + set $grafana_role "Admin"; + } + if ($auth_role = "readwrite") { + set $grafana_role "Editor"; + } + + # Variable allows nginx to start even if service doesn't exist + set $upstream_grafana http://grafana:3000; + proxy_pass $upstream_grafana; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Pass authenticated user and role to Grafana + proxy_set_header X-WEBAUTH-USER $auth_user; + proxy_set_header X-WEBAUTH-ROLE $grafana_role; + proxy_read_timeout 300s; + + # Inject Fleet Management Home button into Grafana HTML + # target="_top" prevents Grafana's SPA router from intercepting the click + sub_filter '' ''; + sub_filter '' '
Home|Grafana
'; + sub_filter_once on; + sub_filter_types text/html; + proxy_set_header Accept-Encoding ""; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # Prometheus - uses variable for runtime DNS resolution + # Prometheus is configured with --web.route-prefix=/prometheus/ so we must preserve the path + location /prometheus { + auth_request /_auth_check; + error_page 401 = @login_redirect; + + # Variable allows nginx to start even if service doesn't exist + set $upstream_prometheus http://prometheus:9090; + proxy_pass $upstream_prometheus; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + + # Inject Fleet Management Home button into Prometheus HTML + sub_filter '' ''; + sub_filter '' '
Home|Prometheus
'; + sub_filter_once on; + sub_filter_types text/html; + proxy_set_header Accept-Encoding ""; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # Vast.ai Exporter metrics endpoint + location /vastai-metrics/ { + auth_request /_auth_check; + error_page 401 = @login_redirect; + + set $upstream_vastai http://vastai-exporter:8622; + proxy_pass $upstream_vastai/metrics; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 30s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # RunPod Exporter metrics endpoint + location /runpod-metrics/ { + auth_request /_auth_check; + error_page 401 = @login_redirect; + + set $upstream_runpod http://runpod-exporter:8623; + proxy_pass $upstream_runpod/metrics; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 30s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + + # Exporter management pages - served by auth server + location /vastai/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + proxy_pass http://127.0.0.1:8081/vastai/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /runpod/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + proxy_pass http://127.0.0.1:8081/runpod/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Exporter account APIs require a current Fleet administrator. + location = /_exporter_admin { + internal; + proxy_pass http://127.0.0.1:8081/auth/exporters/authorize; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Capture the suffix explicitly: variable proxy_pass does not replace + # a prefix URI. Preserve query arguments and never accept a client token. + location ~ ^/vastai-api/(.*)$ { + auth_request /_exporter_admin; + auth_request_set $mgmt_token $upstream_http_x_mgmt_token; + set $upstream_vastai_api http://vastai-exporter:8622; + proxy_pass $upstream_vastai_api/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Mgmt-Token $mgmt_token; + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + location ~ ^/runpod-api/(.*)$ { + auth_request /_exporter_admin; + auth_request_set $mgmt_token $upstream_http_x_mgmt_token; + set $upstream_runpod_api http://runpod-exporter:8623; + proxy_pass $upstream_runpod_api/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Mgmt-Token $mgmt_token; + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + # Default: show landing page + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + } + +} \ No newline at end of file diff --git a/tests/fixtures/main16-self-signed.conf b/tests/fixtures/main16-self-signed.conf new file mode 100644 index 0000000..846048f --- /dev/null +++ b/tests/fixtures/main16-self-signed.conf @@ -0,0 +1,551 @@ +# TRACEWEAVER: file-role=pairing-routing; req=REQ-PAIR-001; trace=TRACE-PAIR-001; ver=VER-PAIR-001 +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + client_max_body_size 100M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + + server { + listen 80; + listen 443 ssl http2; + server_name fleet.example.test; + + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + + # DNS resolver for Docker networks - CRITICAL for runtime resolution + # This allows nginx to start even if backend services don't exist yet + resolver 127.0.0.11 valid=10s ipv6=off; + resolver_timeout 5s; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # ===================================================== + # ERROR HANDLERS + # ===================================================== + + location @service_unavailable { + default_type application/json; + return 503 '{"error":"Service unavailable","message":"The requested service is not running. Install it or wait for it to start."}'; + } + + # ===================================================== + # AUTHENTICATION + # ===================================================== + + # Auth server endpoints (public) + location ^~ /api/account-link/ { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Cookie $http_cookie; + } + + location /auth/ { + proxy_pass http://127.0.0.1:8081/auth/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Internal auth verification endpoint (for auth_request) + location = /_auth_check { + internal; + proxy_pass http://127.0.0.1:8081/auth/headers; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header Cookie $http_cookie; + } + + # ===================================================== + # Internal API (blocked from public access) + # Only reachable via direct Docker network (172.30.0.10:8080) + # ===================================================== + location /internal/ { + return 403; + } + + # ===================================================== + # PUBLIC ENDPOINTS (no auth required) + # ===================================================== + + # Health check API (always public) + location /api/health { + default_type application/json; + return 200 '{"status":"ok","proxy":"running"}'; + } + + # Service health check (internal, no auth) + location /api/services { + proxy_pass http://127.0.0.1:8080/api/services; + proxy_set_header Host $host; + } + + location /api/build-info { + proxy_pass http://127.0.0.1:8080/api/build-info; + } + + # Version management and update endpoints (protected) + location /api/versions { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/versions; + proxy_set_header Host $host; + proxy_connect_timeout 10s; + proxy_read_timeout 30s; + } + + location /api/update-settings { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/update-settings; + proxy_set_header Host $host; + proxy_set_header Content-Type $http_content_type; + proxy_connect_timeout 5s; + proxy_read_timeout 10s; + } + + location /api/update { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/update; + proxy_set_header Host $host; + proxy_set_header Content-Type $http_content_type; + proxy_connect_timeout 10s; + proxy_read_timeout 120s; # Updates can take time + } + + location /api/pull { + auth_request /_auth_check; + error_page 401 = @api_unauthorized; + proxy_pass http://127.0.0.1:8080/api/pull; + proxy_set_header Host $host; + proxy_set_header Content-Type $http_content_type; + proxy_connect_timeout 10s; + proxy_read_timeout 300s; # Image pulls can be slow + } + + location @api_unauthorized { + default_type application/json; + return 401 '{"error":"Authentication required"}'; + } + location /assets/ { + alias /usr/share/nginx/html/assets/; + expires 1h; + } + # Static assets (no auth) + location /static/ { + root /usr/share/nginx/html; + } + + # ===================================================== + # PROTECTED ENDPOINTS (auth required) + # ===================================================== + + # Fleet Management Landing Page (requires auth) + location = / { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + + # Redirect to login if not authenticated + error_page 401 = @login_redirect; + + root /usr/share/nginx/html; + index index.html; + } + + location @login_redirect { + return 302 /auth/login?next=$request_uri; + } + + # VPM calls these endpoints only over the Docker network on port 8081. + # Exact locations keep the generic public /auth/ forwarding route from + # exposing the session authority or password reauthentication API. + location = /auth/vast-price-manager/session { return 404; } + location = /auth/vast-price-manager/session/ { return 404; } + location = /auth/vast-price-manager/reauth { return 404; } + location = /auth/vast-price-manager/reauth/ { return 404; } + # Vast Price Manager has a dedicated admin-only route. It strips the + # prefix once and explicitly preserves the remainder and query string. + location = /_vast_pricing_admin { + internal; + proxy_pass http://127.0.0.1:8081/auth/vast-price-manager/authorize; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Original-URI $request_uri; + } + + location = /vast-pricing { + return 308 /vast-pricing/; + } + + location ~ ^/vast-pricing(?/.*)$ { + auth_request /_vast_pricing_admin; + error_page 401 = @login_redirect; + + # Runtime Docker DNS resolution permits startup before this opt-in + # service has been installed. + set $upstream_vast_price_manager vast-price-manager; + proxy_pass http://$upstream_vast_price_manager:8088$vpm_upstream_path$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + # VPM directly introspects the signed Fleet session on its + # configured internal origin. Never pass client identity headers. + proxy_set_header X-Fleet-Auth-User ""; + proxy_set_header X-Fleet-Auth-Role ""; + proxy_set_header X-Fleet-Auth-Token ""; + proxy_set_header X-Fleet-Authenticated ""; + # Preserve the browser Fleet cookie for VPM's direct introspection. + proxy_set_header Cookie $http_cookie; + # Auth-request failures still use the redirect above. Do not + # intercept VPM's own 401 reauthentication result, which must + # reach its caller as an error rather than a Fleet login redirect. + proxy_intercept_errors off; + error_page 502 503 504 = @service_unavailable; + } + # IPMI Monitor API - returns JSON 401 for unauthenticated requests + location ~ ^/ipmi/api/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @api_unauthorized; + + set $upstream_ipmi http://ipmi-monitor:5000; + proxy_pass $upstream_ipmi/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /ipmi; + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # IPMI Monitor - regex location to properly rewrite URI with variable proxy_pass + location ~ ^/ipmi/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @login_redirect; + + # Variable allows nginx to start even if service doesn't exist + set $upstream_ipmi http://ipmi-monitor:5000; + proxy_pass $upstream_ipmi/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /ipmi; + # Forward authentication headers from proxy auth + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # DC Overview API - returns JSON 401 for unauthenticated requests + location ~ ^/dc/api/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @api_unauthorized; + + set $upstream_dc http://dc-overview:5001; + proxy_pass $upstream_dc/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /dc; + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # DC Overview / Server Manager - regex location to properly rewrite URI with variable proxy_pass + location ~ ^/dc/(.*)$ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + auth_request_set $auth_token $upstream_http_x_fleet_auth_token; + error_page 401 = @login_redirect; + + # Variable allows nginx to start even if service doesn't exist + set $upstream_dc http://dc-overview:5001; + proxy_pass $upstream_dc/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /dc; + # Forward authentication headers from proxy auth + proxy_set_header X-Fleet-Auth-User $auth_user; + proxy_set_header X-Fleet-Auth-Role $auth_role; + proxy_set_header X-Fleet-Auth-Token $auth_token; + proxy_set_header X-Fleet-Authenticated true; + proxy_read_timeout 300s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # Shortcuts for /servers and /settings -> /dc/... + location = /servers { + return 301 /dc/servers; + } + location = /settings { + return 301 /dc/settings; + } + + # Grafana - uses variable for runtime DNS resolution + location /grafana/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + + # Map Fleet roles to Grafana roles + set $grafana_role "Viewer"; + if ($auth_role = "admin") { + set $grafana_role "Admin"; + } + if ($auth_role = "readwrite") { + set $grafana_role "Editor"; + } + + # Variable allows nginx to start even if service doesn't exist + set $upstream_grafana http://grafana:3000; + proxy_pass $upstream_grafana; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Pass authenticated user and role to Grafana + proxy_set_header X-WEBAUTH-USER $auth_user; + proxy_set_header X-WEBAUTH-ROLE $grafana_role; + proxy_read_timeout 300s; + + # Inject Fleet Management Home button into Grafana HTML + # target="_top" prevents Grafana's SPA router from intercepting the click + sub_filter '' ''; + sub_filter '' '
Home|Grafana
'; + sub_filter_once on; + sub_filter_types text/html; + proxy_set_header Accept-Encoding ""; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # Prometheus - uses variable for runtime DNS resolution + # Prometheus is configured with --web.route-prefix=/prometheus/ so we must preserve the path + location /prometheus { + auth_request /_auth_check; + error_page 401 = @login_redirect; + + # Variable allows nginx to start even if service doesn't exist + set $upstream_prometheus http://prometheus:9090; + proxy_pass $upstream_prometheus; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + + # Inject Fleet Management Home button into Prometheus HTML + sub_filter '' ''; + sub_filter '' '
Home|Prometheus
'; + sub_filter_once on; + sub_filter_types text/html; + proxy_set_header Accept-Encoding ""; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # Vast.ai Exporter metrics endpoint + location /vastai-metrics/ { + auth_request /_auth_check; + error_page 401 = @login_redirect; + + set $upstream_vastai http://vastai-exporter:8622; + proxy_pass $upstream_vastai/metrics; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 30s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + # RunPod Exporter metrics endpoint + location /runpod-metrics/ { + auth_request /_auth_check; + error_page 401 = @login_redirect; + + set $upstream_runpod http://runpod-exporter:8623; + proxy_pass $upstream_runpod/metrics; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 30s; + + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + # Vast Price Manager uses its dedicated admin-only route above. + + # Exporter management pages - served by auth server + location /vastai/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + proxy_pass http://127.0.0.1:8081/vastai/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /runpod/ { + auth_request /_auth_check; + auth_request_set $auth_user $upstream_http_x_fleet_auth_user; + auth_request_set $auth_role $upstream_http_x_fleet_auth_role; + error_page 401 = @login_redirect; + proxy_pass http://127.0.0.1:8081/runpod/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Exporter account APIs require a current Fleet administrator. + location = /_exporter_admin { + internal; + proxy_pass http://127.0.0.1:8081/auth/exporters/authorize; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Capture the suffix explicitly: variable proxy_pass does not replace + # a prefix URI. Preserve query arguments and never accept a client token. + location ~ ^/vastai-api/(.*)$ { + auth_request /_exporter_admin; + auth_request_set $mgmt_token $upstream_http_x_mgmt_token; + set $upstream_vastai_api http://vastai-exporter:8622; + proxy_pass $upstream_vastai_api/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Mgmt-Token $mgmt_token; + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + + location ~ ^/runpod-api/(.*)$ { + auth_request /_exporter_admin; + auth_request_set $mgmt_token $upstream_http_x_mgmt_token; + set $upstream_runpod_api http://runpod-exporter:8623; + proxy_pass $upstream_runpod_api/api/$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Mgmt-Token $mgmt_token; + proxy_intercept_errors on; + error_page 502 503 504 = @service_unavailable; + } + # Default: show landing page + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + } + +} \ No newline at end of file diff --git a/tests/test_standard_adoption.py b/tests/test_standard_adoption.py index b177d5c..93a8def 100644 --- a/tests/test_standard_adoption.py +++ b/tests/test_standard_adoption.py @@ -585,22 +585,73 @@ def test_recover_adoption_reports_retained_success_only_with_live_identity(tmp_p assert adoption.recover_adoption(plan['plan_id'])['state'] == 'recovery-required' -def test_previous_standard_profile_is_exact_and_adoptable(tmp_path,monkeypatch): - import subprocess +@pytest.mark.parametrize('profile,tls', [ + ('standard-444d7c0','self-signed'), + ('standard-8e2b421','self-signed'), + ('standard-8e2b421','letsencrypt'), +]) +def test_previous_standard_profile_is_exact_and_adoptable(tmp_path,monkeypatch,profile,tls): from cryptolabs_proxy.config import get_jinja_env from cryptolabs_proxy.standard_adoption import AdoptionError adoption,engine,settings,directory=system(tmp_path,monkeypatch) - raw=(Path(__file__).parents[1]/'src/cryptolabs_proxy/templates/previous-standard-nginx-444d7c0.conf.j2').read_bytes() - assert hashlib.sha256(raw).hexdigest()=='f12e2b9800cbaa0e49e685506cfaad38eb7e568fa6d239fa15277af59073d8de' - template=raw.decode() - old=get_jinja_env().from_string(template).render(domain=settings['domain'],letsencrypt=False,services=settings['services'],host_forward_groups=[],acme_challenge=None,hostname_policy=None,http_policy=None).encode() + settings['tls']=tls + if profile=='standard-444d7c0': + raw=(Path(__file__).parents[1]/'src/cryptolabs_proxy/templates/previous-standard-nginx-444d7c0.conf.j2').read_bytes() + assert hashlib.sha256(raw).hexdigest()=='f12e2b9800cbaa0e49e685506cfaad38eb7e568fa6d239fa15277af59073d8de' + old=get_jinja_env().from_string(raw.decode()).render(domain=settings['domain'],letsencrypt=False,services=settings['services'],host_forward_groups=[],acme_challenge=None,hostname_policy=None,http_policy=None).encode() + else: + old=(Path(__file__).parent/'fixtures'/('main16-'+tls+'.conf')).read_bytes() + expected={'self-signed':'f1f570ca848b5dbf15d635bf4d3de90cbb89a68e2c9da0761d6295b7d46a80b6','letsencrypt':'5cad780fddc4aea67b2a13e26b808e9b1454f9a33abd6d459b82d8066423cbb5'} + assert hashlib.sha256(old).hexdigest()==expected[tls] + assert b'location ^~ /api/account-link/ {' in old engine.config=old plan=adoption.plan(settings) - assert plan['source_profile']=='standard-444d7c0' + assert plan['source_profile']==profile engine.config=old.replace(b'keepalive_timeout 65;',b'keepalive_timeout 66;') - with pytest.raises(AdoptionError):adoption.plan(settings) + with pytest.raises(AdoptionError,match='unsupported_source_behavior'):adoption.plan(settings) + assert engine.calls==[] engine.config=old + original_auth=engine.auth assert adoption.adopt(settings,plan['plan_id'])['state']=='adopted' + assert engine.auth==original_auth + assert b'location ^~ /api/account-link/ {' in engine.config + assert b'location ^~ /api/releases/ {' in engine.config + assert b'location = /api/update {' in engine.config + certificate=(b'/etc/letsencrypt/live/fleet.example.test/fullchain.pem' if tls=='letsencrypt' else b'/etc/nginx/ssl/server.crt') + assert b'ssl_certificate '+certificate+b';' in engine.config + assert next(m['Source'] for m in engine.request['HostConfig']['Mounts'] if m['Target']=='/data/auth')=='old-auth' + assert any(m['Target']=='/etc/letsencrypt' and m['Source']=='certs' for m in engine.request['HostConfig']['Mounts']) + + +def test_main16_unknown_source_directive_is_refused_without_mutation(tmp_path,monkeypatch): + from cryptolabs_proxy.standard_adoption import AdoptionError + adoption,engine,settings,directory=system(tmp_path,monkeypatch) + source=(Path(__file__).parent/'fixtures/main16-self-signed.conf').read_bytes() + assert b'keepalive_timeout 65;' in source + engine.config=source.replace(b'keepalive_timeout 65;',b'keepalive_timeout 66;') + before={p.name:p.read_bytes() for p in directory.iterdir()} + runtime=deepcopy(engine.current) + with pytest.raises(AdoptionError,match='unsupported_source_behavior'):adoption.plan(settings) + assert engine.calls==[] and engine.current==runtime + assert before=={p.name:p.read_bytes() for p in directory.iterdir()} + + +def test_main16_corrupt_profile_asset_is_refused_without_mutation(tmp_path,monkeypatch): + from jinja2 import PackageLoader + from cryptolabs_proxy.standard_adoption import AdoptionError + adoption,engine,settings,directory=system(tmp_path,monkeypatch) + engine.config=(Path(__file__).parent/'fixtures/main16-self-signed.conf').read_bytes() + get_source=PackageLoader.get_source + def corrupt_main16(loader,environment,template): + source,filename,uptodate=get_source(loader,environment,template) + if template=='previous-standard-nginx-8e2b421.conf.j2':source+='\n# corrupted stock profile' + return source,filename,uptodate + monkeypatch.setattr(PackageLoader,'get_source',corrupt_main16) + before={p.name:p.read_bytes() for p in directory.iterdir()} + runtime=deepcopy(engine.current) + with pytest.raises(AdoptionError,match='previous_standard_profile_unavailable'):adoption.plan(settings) + assert engine.calls==[] and engine.current==runtime + assert before=={p.name:p.read_bytes() for p in directory.iterdir()} @pytest.mark.parametrize('enabled',[True,False,'true']) diff --git a/traceability-matrix.md b/traceability-matrix.md index 2ae8d20..ed1ee9a 100644 --- a/traceability-matrix.md +++ b/traceability-matrix.md @@ -43,6 +43,7 @@ Implementation candidate; all installed/runtime/native/browser/requirement closu | src/cryptolabs_proxy/standard_adoption.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | entrypoint | fleet-release-recovery | | src/cryptolabs_proxy/config.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | file-role | fleet-release-compose-consumer | | src/cryptolabs_proxy/config.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | entrypoint | fleet-release-compose-consumer | +| src/cryptolabs_proxy/standard_adoption.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | entrypoint | fleet-stock-profile-renderer | | src/cryptolabs_proxy/update_recovery.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | file-role | native-service-recovery | | src/cryptolabs_proxy/update_recovery.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | entrypoint | native-service-recovery | | src/cryptolabs_proxy/updates.py | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | entrypoint | fleet-release-implementation | @@ -113,3 +114,25 @@ hashes are retained at `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/e Shared pre-code V&V is `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/go/recovery-vv-root-reviewed.json`. Native installed-wheel bootstrap, source publication, production reconciliation, site continuity, signed admission and UI acceptance remain held. + + +## Main 1.6 stock-profile compatibility evidence + +FLEET-MAIN16-COMPAT-20261102 retains the existing REQ/TRACE/VER/VAL-FLEETUPDATE-001 +labels and owner-approved amendment159af842. Accepted authority and root source GO: +`../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/plan.json` and `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/root-source-go.json`. +Verification: actual preimplementation RED in `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/definition/worktree-red-evidence.json`, +then initial source/package evidence in `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/build/`. Local GREEN does not close +VAL-FLEETUPDATE-001 or installed/native/browser acceptance; final independent review remains held. + +The following immutable copied/generated data exceptions preserve released producer bytes, +including existing source comments and whitespace. No inserted anchors or byte normalization +are permitted. Exact Main template Git blob is `d1dd2d87eaa5f696d610997f522a983026b3ef6d`; +producer/template dependency identities are in `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/definition/producer-provenance.json`. +Root approved the frozen fixture scope in `../artifacts/ipmi-readiness-20261026/fleet-ui-20261102/execution/main16-compat/plan/root-test-only-go.json`. + +| Artifact Path | Requirement | Trace | Verification | Anchor Type | Provenance / acceptance | +| --- | --- | --- | --- | --- | --- | +| src/cryptolabs_proxy/templates/previous-standard-nginx-8e2b421.conf.j2 | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | Exception | Immutable released Main producer data; SHA256 05828b78143c5d6cd7cbe60700cfc54b627cb4efff9a7af43439fdb56e25edab; exact-byte provenance and adoption/corruption tests; native/site acceptance held | +| tests/fixtures/main16-self-signed.conf | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | Exception | Immutable released Main producer data; SHA256 f1f570ca848b5dbf15d635bf4d3de90cbb89a68e2c9da0761d6295b7d46a80b6; exact-byte provenance and adoption/corruption tests; native/site acceptance held | +| tests/fixtures/main16-letsencrypt.conf | REQ-FLEETUPDATE-001 | TRACE-FLEETUPDATE-001 | VER-FLEETUPDATE-001 | Exception | Immutable released Main producer data; SHA256 5cad780fddc4aea67b2a13e26b808e9b1454f9a33abd6d459b82d8066423cbb5; exact-byte provenance and adoption/corruption tests; native/site acceptance held |