rcf-lite 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/blueprints/application-account-settings/docs/topics.md +3 -0
- package/blueprints/application-admin-console/docs/topics.md +3 -0
- package/blueprints/application-api-rest/docs/topics.md +3 -0
- package/blueprints/application-charts/docs/topics.md +3 -0
- package/blueprints/application-dashboard/docs/topics.md +3 -0
- package/blueprints/application-datatable/docs/topics.md +3 -0
- package/blueprints/application-empty-error-states/docs/topics.md +3 -0
- package/blueprints/application-error-handling/docs/topics.md +3 -0
- package/blueprints/application-file-upload/docs/topics.md +3 -0
- package/blueprints/application-forms-wizard/docs/topics.md +3 -0
- package/blueprints/application-notifications-in-app/docs/topics.md +3 -0
- package/blueprints/application-onboarding-tour/docs/topics.md +3 -0
- package/blueprints/application-spa/docs/topics.md +3 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +3 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -0
- package/blueprints/deploy-hetzner-server/CHANGELOG.md +34 -0
- package/blueprints/deploy-hetzner-server/README.md +202 -0
- package/blueprints/deploy-hetzner-server/blueprint.json +233 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +20 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +64 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +116 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +180 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +106 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +63 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +60 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +58 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-cloud-init-render-lint.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-hcloud-dry-run-mock.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-cloud-init-hardened.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-snapshot-on-demand.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-throwaway-server-provision.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-001.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-002.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-003.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-004.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-005.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-006.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json +108 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3801-deploy-hetzner-server-provisioner.json +27 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +93 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37101.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37102.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37103.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37104.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37105.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37106.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37107.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37108.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37109.json +24 -0
- package/blueprints/deploy-hetzner-server/docs/topics.md +48 -0
- package/blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md +180 -0
- package/blueprints/edge-cloudflare-access/docs/topics.md +3 -0
- package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +3 -0
- package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +13 -0
- package/blueprints/edge-cloudflare-tunnel/README.md +257 -0
- package/blueprints/edge-cloudflare-tunnel/blueprint.json +168 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +23 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/aud-presence-check.mjs +114 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/cloudflared-config-lint.mjs +80 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +234 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/probe-utils.mjs +231 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +54 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +73 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-aud-presence-check.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-cloudflared-config-lint.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-manifest-schema-validate.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-connector-healthy.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-tunnel-hostname-routes.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/tunnel-manifest.schema.json +55 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-001.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-002.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-003.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-004.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-005.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +25 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39101.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39102.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39103.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39104.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39105.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39106.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39107.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39108.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/docs/topics.md +72 -0
- package/blueprints/edge-cloudflare-tunnel/guide/edge-cloudflare-tunnel.md +204 -0
- package/blueprints/edge-cloudflare-turnstile/docs/topics.md +3 -0
- package/blueprints/email-smtp-resend/docs/topics.md +3 -0
- package/blueprints/jobs-background/docs/topics.md +3 -0
- package/blueprints/messaging-queue-cloudflare/docs/topics.md +3 -0
- package/blueprints/object-storage-s3/docs/topics.md +3 -0
- package/blueprints/observability-essentials/docs/topics.md +3 -0
- package/blueprints/observability-logging/docs/topics.md +3 -0
- package/blueprints/observability-probe-endpoints/docs/topics.md +3 -0
- package/blueprints/persistence-data-d1/docs/topics.md +3 -0
- package/blueprints/persistence-data-postgres/docs/topics.md +3 -0
- package/blueprints/persistence-data-sqlite/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-kv/docs/topics.md +3 -0
- package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
- package/blueprints/platform-docker-compose-host/README.md +76 -0
- package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
- package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
- package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
- package/blueprints/security-auth-clerk/docs/topics.md +3 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +3 -0
- package/blueprints/security-auth-magic-link/docs/topics.md +3 -0
- package/blueprints/security-auth-oauth2/docs/topics.md +3 -0
- package/blueprints/security-secrets-management/docs/topics.md +3 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
- package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
- package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
- package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
- package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
- package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
- package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
- package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
- package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
- package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
- package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
- package/rcf/code-nodes/cn-410.json +20 -0
- package/rcf/code-nodes/cn-440.json +20 -0
- package/rcf/code-nodes/cn-470.json +19 -0
- package/rcf/fbs/fbs-130.json +23 -0
- package/rcf/fbs/fbs-140.json +23 -0
- package/rcf/fbs/fbs-150.json +22 -0
- package/rcf/requirements/req-110.json +58 -0
- package/rcf/requirements/req-111.json +32 -0
- package/rcf/requirements/req-112.json +42 -0
- package/rcf/requirements/req-113.json +33 -0
- package/rcf/requirements/req-114.json +20 -0
- package/rcf/requirements/req-115.json +38 -0
- package/rcf/requirements/req-120.json +20 -0
- package/rcf/requirements/req-121.json +20 -0
- package/rcf/requirements/req-122.json +20 -0
- package/rcf/requirements/req-123.json +42 -0
- package/rcf/requirements/req-124.json +27 -0
- package/rcf/requirements/req-125.json +20 -0
- package/rcf/requirements/req-130.json +32 -0
- package/rcf/requirements/req-131.json +54 -0
- package/rcf/requirements/req-132.json +27 -0
- package/rcf/requirements/req-133.json +20 -0
- package/rcf/requirements/req-134.json +42 -0
- package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
- package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
- package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
- package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
- package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
- package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
- package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
- package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
- package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
- package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
- package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
- package/rcf/test-suites/ts-140.json +86 -0
- package/rcf/test-suites/ts-150.json +86 -0
- package/rcf/test-suites/ts-160.json +78 -0
- package/rcf/user-stories/us-11001.json +23 -0
- package/rcf/user-stories/us-11101.json +23 -0
- package/rcf/user-stories/us-11102.json +24 -0
- package/rcf/user-stories/us-11201.json +23 -0
- package/rcf/user-stories/us-11202.json +23 -0
- package/rcf/user-stories/us-11301.json +24 -0
- package/rcf/user-stories/us-11401.json +23 -0
- package/rcf/user-stories/us-11402.json +23 -0
- package/rcf/user-stories/us-11501.json +23 -0
- package/rcf/user-stories/us-12001.json +23 -0
- package/rcf/user-stories/us-12101.json +23 -0
- package/rcf/user-stories/us-12102.json +23 -0
- package/rcf/user-stories/us-12201.json +23 -0
- package/rcf/user-stories/us-12202.json +24 -0
- package/rcf/user-stories/us-12301.json +23 -0
- package/rcf/user-stories/us-12401.json +23 -0
- package/rcf/user-stories/us-12402.json +23 -0
- package/rcf/user-stories/us-12501.json +23 -0
- package/rcf/user-stories/us-13001.json +23 -0
- package/rcf/user-stories/us-13002.json +23 -0
- package/rcf/user-stories/us-13101.json +23 -0
- package/rcf/user-stories/us-13102.json +23 -0
- package/rcf/user-stories/us-13201.json +23 -0
- package/rcf/user-stories/us-13301.json +23 -0
- package/rcf/user-stories/us-13302.json +23 -0
- package/rcf/user-stories/us-13401.json +23 -0
- package/releases/releases.yaml +11 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# edge-cloudflare-tunnel CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 1.0.0 - 2026-09-08
|
|
4
|
+
|
|
5
|
+
Initial release. Round-7 T-3 of the Hetzner spec at `projects/blueprint-library/specs/hetzner-round-7-spec-2026-09-07.md`.
|
|
6
|
+
|
|
7
|
+
- Mints capability `tunnelBridge` and global topic `edgeIngressBridge`.
|
|
8
|
+
- Five REQs, eight USs, three TACs, three ADRs; five Node-only probes.
|
|
9
|
+
- Extends the shared throwaway-Hetzner-server fixture with a cloudflared connector in both runtime shapes (compose-service alongside the T-2 web and caddy services when `containerHost` is applied; systemd-unit for a bare `cloudHost`) and both hostname modes (access-gated when `zeroTrustGate` is applied; public-hostname when absent) plus five `run-<probe>.mjs` delegate shims.
|
|
10
|
+
- manifest-schema-validate carries five fixture-side mutation switches (`SIMULATE_MANIFEST_INVALID_TUNNEL_ID`, `SIMULATE_MANIFEST_CREDENTIALS_INLINE`, `SIMULATE_MANIFEST_MISSING_CATCHALL`, `SIMULATE_ORIGIN_PORT_OPEN`, `SIMULATE_EVENT_SECRECY_LEAK`); cloudflared-config-lint carries `SIMULATE_INGRESS_INVALID` and runs `cloudflared tunnel ingress validate` via the `cloudflare/cloudflared:2026.8.3` container when a local binary is not on PATH; aud-presence-check carries `SIMULATE_AUD_DROP`.
|
|
11
|
+
- Mutation-discipline (T-2 gate ruling carried forward): every `SIMULATE_*` switch lives in the fixture-side delegate shim; the probe modules read only `RCF_LITE_T3_FIXTURE_ROOT` and never branch on being under mutation.
|
|
12
|
+
- Connector-runtime choice elicited via `connector-runtime` (compose-service default when `containerHost` applied; systemd-unit alternative for bare `cloudHost`, per the vendor cloudflared as-a-service local-configuration-file docs); hostname-mode discovered (not elicited) from `appliedCapabilities` via the sidecar per ADR-4003.
|
|
13
|
+
- Consumers: `w-2026-09-06-dave-010` follow-on (make the librarian API and workspace viewer reachable without Mullvad) is the estate consumer; blueprint acceptance never depends on that consumer, and the ops-01 migration mints as its own work item.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# edge-cloudflare-tunnel
|
|
2
|
+
|
|
3
|
+
Round-7 T-3 core blueprint on the rcf-lite shelf. Bridges a service on a
|
|
4
|
+
`cloudHost` (bare, systemd shape) or a `containerHost` (compose-service
|
|
5
|
+
shape) to the Cloudflare edge through a Cloudflare Tunnel, with no
|
|
6
|
+
public origin ports. When applied alongside `edge-cloudflare-access`
|
|
7
|
+
(via the `zeroTrustGate` capability) the tunnel binds an Access AUD;
|
|
8
|
+
when applied without it the tunnel serves in public-hostname mode.
|
|
9
|
+
|
|
10
|
+
- Slug: `edge-cloudflare-tunnel`
|
|
11
|
+
- Version: `1.0.0`
|
|
12
|
+
- Category: `edge`
|
|
13
|
+
- Capabilities: `["tunnelBridge"]`
|
|
14
|
+
- Mints global topic: `edgeIngressBridge`
|
|
15
|
+
- Contribution count: 5 REQs, 8 USs, 3 TACs, 3 ADRs, 5 probes
|
|
16
|
+
- Consumers: ops-01 has no consumer today (SSH is the only inbound path). Estate consumer is the follow-on make-the-librarian-API-and-workspace-viewer-reachable-without-Mullvad work that `w-2026-09-06-dave-010` is expected to green-light. Blueprint acceptance never depends on that consumer; the ops-01 migration mints as a separate work item at that point (`w-2026-09-07-dave-004`).
|
|
17
|
+
|
|
18
|
+
## Five REQs
|
|
19
|
+
|
|
20
|
+
| Id | Contract |
|
|
21
|
+
|---|---|
|
|
22
|
+
| edge-cloudflare-tunnel-REQ-001 | Connector install shape switches on applied `containerHost` (compose-service default; systemd-unit for bare `cloudHost`); `cloudflaredReady` fires on boot with connector id, region and version metadata. |
|
|
23
|
+
| edge-cloudflare-tunnel-REQ-002 | Tunnel manifest schema at `cloudflare/tunnels/<name>.yaml` (uuid tunnel id, `credentialsFile.secretRef` reference, ingress rules with catch-all `http_status:404`). |
|
|
24
|
+
| edge-cloudflare-tunnel-REQ-003 | No public origin ports on the host; T-1 firewall stays closed on 80/443 unless a reverse proxy is applied; manifest refuses ingress rules whose service URL binds to a host public interface. |
|
|
25
|
+
| edge-cloudflare-tunnel-REQ-004 | Access-gated composition: `zeroTrustGate` applied attaches `originRequest.access.aud`; absent omits the block and serves in public-hostname mode. |
|
|
26
|
+
| edge-cloudflare-tunnel-REQ-005 | Credentials-file discipline (host mode 0o400, referenced via `secretRef`, not committed, grep-refuse, no leak in event bodies). |
|
|
27
|
+
|
|
28
|
+
## Connector runtime shapes
|
|
29
|
+
|
|
30
|
+
The connector runtime is elicited via `connector-runtime` (ADR-4002) with
|
|
31
|
+
`recommendedDefault: compose-service`.
|
|
32
|
+
|
|
33
|
+
- **compose-service** (default when `containerHost` is applied). The
|
|
34
|
+
cloudflared container ships alongside the applying project services on
|
|
35
|
+
the same web-net network. No host ports; restart `unless-stopped`;
|
|
36
|
+
logging driver aligned with the T-2 compose-host log-driver elicit.
|
|
37
|
+
Fixture: `packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/compose-service/`.
|
|
38
|
+
- **systemd-unit** (alternative for a bare `cloudHost`). The cloudflared
|
|
39
|
+
binary is installed via the vendor package per the cloudflared
|
|
40
|
+
as-a-service documented systemd unit and runs as a systemd service
|
|
41
|
+
reading the local configuration file. Fixture:
|
|
42
|
+
`packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/systemd-unit/`.
|
|
43
|
+
|
|
44
|
+
When to reach for which: pick compose-service when the project already
|
|
45
|
+
runs docker compose on the host (natural home, same log driver, same
|
|
46
|
+
restart discipline); pick systemd-unit when the host runs bare and there
|
|
47
|
+
is no compose stack to add a service to (or when the project deliberately
|
|
48
|
+
keeps cloudflared outside the compose lifecycle for boot ordering).
|
|
49
|
+
|
|
50
|
+
## Tunnel manifest shape
|
|
51
|
+
|
|
52
|
+
The manifest lives at `cloudflare/tunnels/<name>.yaml` under the
|
|
53
|
+
applying project. The shipped JSON schema
|
|
54
|
+
(`contributions/probes/tunnel-manifest.schema.json`) requires:
|
|
55
|
+
|
|
56
|
+
- `tunnel`: uuid-shaped Cloudflare Tunnel id.
|
|
57
|
+
- `credentialsFile.secretRef`: reference into `security-secrets-management`
|
|
58
|
+
(never a literal path or JSON body inline).
|
|
59
|
+
- `ingress`: array of `{ hostname?, service, originRequest? }`. The last
|
|
60
|
+
entry MUST be a catch-all `service: http_status:404`. When the
|
|
61
|
+
Access-gated shape is applied every non-catch-all rule attaches
|
|
62
|
+
`originRequest.access.aud` with the AUD from the sidecar.
|
|
63
|
+
|
|
64
|
+
Example (compose-service, public-hostname mode):
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
tunnel: 00000000-0000-4000-8000-0000000012ab
|
|
68
|
+
credentialsFile:
|
|
69
|
+
secretRef: hq-estate/CLOUDFLARE_TUNNEL_CREDENTIALS_MY_PROJECT
|
|
70
|
+
ingress:
|
|
71
|
+
- hostname: my-app.example.com
|
|
72
|
+
service: http://web:8080
|
|
73
|
+
- service: http_status:404
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Example (Access-gated shape adds one block per non-catch-all rule):
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
ingress:
|
|
80
|
+
- hostname: my-app.example.com
|
|
81
|
+
service: http://web:8080
|
|
82
|
+
originRequest:
|
|
83
|
+
access:
|
|
84
|
+
aud: AUD-example-cloudflare-team
|
|
85
|
+
teamName: my-team
|
|
86
|
+
required: true
|
|
87
|
+
- service: http_status:404
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Credentials-file discipline
|
|
91
|
+
|
|
92
|
+
The tunnel credentials JSON file lands on the host at mode 0o400 (owner
|
|
93
|
+
read only). The manifest references it as a `credentialsFile.secretRef`;
|
|
94
|
+
the fixture placeholder at
|
|
95
|
+
`packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/*/credentials/probe.json.example`
|
|
96
|
+
documents the pattern. The credentials file MUST NOT be committed to
|
|
97
|
+
the applying project tree; a grep across the working tree for the
|
|
98
|
+
tunnel id and account tag literals refuses on any tracked file. Every
|
|
99
|
+
`cloudflaredReady`, `tunnelConnectorUp` and `ingressRuleReloaded` event
|
|
100
|
+
body carries metadata only; the event-secrecy scan refuses on a body
|
|
101
|
+
that mentions the credentials literal.
|
|
102
|
+
|
|
103
|
+
Rotate credentials by rewriting the secret pointed to by `secretRef`
|
|
104
|
+
and re-applying; the connector reloads on the next boot cycle.
|
|
105
|
+
|
|
106
|
+
## Composition point (Access-gated vs public-hostname)
|
|
107
|
+
|
|
108
|
+
The hostname mode is discovered from the applied capability set (not
|
|
109
|
+
elicited; see ADR-4003). The blueprint reads the sidecar at
|
|
110
|
+
`rcf/blueprints/edge-cloudflare-tunnel.applied.json` and inspects
|
|
111
|
+
`appliedCapabilities`.
|
|
112
|
+
|
|
113
|
+
- When `zeroTrustGate` is present (i.e. `edge-cloudflare-access` v1.0.0
|
|
114
|
+
is applied on the same project), every ingress rule attaches
|
|
115
|
+
`originRequest.access.aud` and cloudflared refuses at the edge any
|
|
116
|
+
request without a valid Access JWT for that AUD.
|
|
117
|
+
- When `zeroTrustGate` is absent, the block is omitted and the tunnel
|
|
118
|
+
serves in public-hostname mode (open ingress on the public hostname;
|
|
119
|
+
authorization is whatever the origin service itself carries).
|
|
120
|
+
|
|
121
|
+
The `aud-presence-check` probe refuses when the gated variant drifts to
|
|
122
|
+
omit the AUD block (defensive against silent degradation).
|
|
123
|
+
|
|
124
|
+
Graceful degrade: when Access is not applied (T-4 absent from
|
|
125
|
+
`origin/main` or the applying project simply does not want the gate),
|
|
126
|
+
the blueprint still ships in public-hostname mode. The probe assertion
|
|
127
|
+
runs on both fixture variants so the shape flip is proven on every
|
|
128
|
+
reviewer boot.
|
|
129
|
+
|
|
130
|
+
## No-public-ports rule
|
|
131
|
+
|
|
132
|
+
The T-1 firewall on a `cloudHost` allows 22 unconditionally and 80/443
|
|
133
|
+
only when a reverse proxy is applied. A project applying only T-1 plus
|
|
134
|
+
T-3 (no reverse proxy) declares zero 80/443 rules; the tunnel is the
|
|
135
|
+
sole path from the edge to the origin. The manifest schema refuses
|
|
136
|
+
ingress rules whose service URL binds to `0.0.0.0`, a non-loopback
|
|
137
|
+
numeric IP, or an unresolvable shape.
|
|
138
|
+
|
|
139
|
+
## Elicited parameters
|
|
140
|
+
|
|
141
|
+
- `tunnel-name` (string; default `probe`): the Cloudflare Tunnel name
|
|
142
|
+
and the manifest filename under `cloudflare/tunnels/<name>.yaml`.
|
|
143
|
+
- `cloudflare-zone-secret` (string; default `hq-estate/CLOUDFLARE_ZONE_ID`):
|
|
144
|
+
`security-secrets-management` reference to the Cloudflare zone the
|
|
145
|
+
tunnel serves under.
|
|
146
|
+
- `public-hostname-template` (string; default `<service>.<zone>`):
|
|
147
|
+
template for the public hostname, expanded per ingress rule.
|
|
148
|
+
- `ingress-rules` (string; default `[]`): ingress rules array (hostname
|
|
149
|
+
+ service URL pairs).
|
|
150
|
+
- `access-aud` (string; default empty): Access AUD tag for the gated
|
|
151
|
+
shape. Presence toggles the gated shape when `zeroTrustGate` is
|
|
152
|
+
applied.
|
|
153
|
+
- `connector-runtime` (enum: `compose-service`, `systemd-unit`; default
|
|
154
|
+
`compose-service`): the connector runtime per ADR-4002.
|
|
155
|
+
|
|
156
|
+
## Companions
|
|
157
|
+
|
|
158
|
+
- `suggestedCompanions[logging]`: every `cloudflaredReady`,
|
|
159
|
+
`tunnelConnectorUp`, `tunnelConnectorDown`, `ingressRuleReloaded` and
|
|
160
|
+
`accessGateRefused` event writes through the applied logger.
|
|
161
|
+
- `suggestedCompanions[errorHandling]`: a cloudflared exit, a JWT-check
|
|
162
|
+
failure, an ingress-rule schema violation, a hostname-DNS-route API
|
|
163
|
+
refusal constructs an internal error record.
|
|
164
|
+
|
|
165
|
+
`providesRoles: []`.
|
|
166
|
+
|
|
167
|
+
## Five probes
|
|
168
|
+
|
|
169
|
+
Every probe writes its report envelope to
|
|
170
|
+
`.rcf/reports/blueprints/edge-cloudflare-tunnel/<probe-name>.json`
|
|
171
|
+
under the repo root.
|
|
172
|
+
|
|
173
|
+
- **manifest-schema-validate** (local). Anchors AC-tunnel-manifestSchema,
|
|
174
|
+
AC-tunnel-noPublicOrigin, AC-tunnel-credentialsDiscipline. `accountBound: false`.
|
|
175
|
+
Ajv-free walker over the shipped JSON schema; runs on all four
|
|
176
|
+
variants; refuses on `SIMULATE_MANIFEST_INVALID_TUNNEL_ID`,
|
|
177
|
+
`SIMULATE_MANIFEST_CREDENTIALS_INLINE`, `SIMULATE_MANIFEST_MISSING_CATCHALL`,
|
|
178
|
+
`SIMULATE_ORIGIN_PORT_OPEN`, `SIMULATE_EVENT_SECRECY_LEAK`.
|
|
179
|
+
- **cloudflared-config-lint** (local). Anchors AC-tunnel-hostnameRoutes.
|
|
180
|
+
`accountBound: false`. Runs `cloudflared tunnel --config <path> ingress validate`
|
|
181
|
+
via a local binary or the vendor container image
|
|
182
|
+
`cloudflare/cloudflared:2026.8.3`; refuses on `SIMULATE_INGRESS_INVALID`.
|
|
183
|
+
- **aud-presence-check** (local). Anchors AC-tunnel-accessGated,
|
|
184
|
+
AC-tunnel-accessGatedRefuse. `accountBound: false`. Reads the sidecar
|
|
185
|
+
and the paired manifests; asserts the shape flips correctly; refuses
|
|
186
|
+
on `SIMULATE_AUD_DROP` when the gated fixture drifts to open ingress.
|
|
187
|
+
- **real-account-connector-healthy** (account-bound). Anchors
|
|
188
|
+
AC-tunnel-connectorHealthy. `accountBound: true` on
|
|
189
|
+
`CI_HAS_CLOUDFLARE_ACCOUNT` AND `CI_HAS_HETZNER_ACCOUNT`. Without
|
|
190
|
+
both, records `accountBoundSkipped: true` and the aggregate flips to
|
|
191
|
+
`pass`.
|
|
192
|
+
- **real-account-tunnel-hostname-routes** (account-bound). Anchors
|
|
193
|
+
AC-tunnel-hostnameRoutes (public sub-case) and AC-tunnel-accessGated
|
|
194
|
+
(gated sub-case). `accountBound: true`. The public-hostname sub-case
|
|
195
|
+
runs on `CI_HAS_CLOUDFLARE_ACCOUNT` + `CI_HAS_HETZNER_ACCOUNT`; the
|
|
196
|
+
access-gated sub-case adds `CI_HAS_CLOUDFLARE_ACCESS` per the round-7
|
|
197
|
+
Q4 ratification. Sub-cases skip independently.
|
|
198
|
+
|
|
199
|
+
The delegate shims at
|
|
200
|
+
`packages/rcf-lite/test/fixtures/hetzner-throwaway-server/run-<probe>.mjs`
|
|
201
|
+
read the `SIMULATE_*` env vars and prepare mutated inputs in a scratch
|
|
202
|
+
dir; the probe modules never read a `SIMULATE_` env var and never
|
|
203
|
+
branch on being under mutation.
|
|
204
|
+
|
|
205
|
+
## Two fixture variants times two hostname modes
|
|
206
|
+
|
|
207
|
+
The fixture lives under
|
|
208
|
+
`packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/`:
|
|
209
|
+
|
|
210
|
+
- `compose-service/compose-fragment.yaml`: the cloudflared compose
|
|
211
|
+
service alongside the T-2 web and caddy services.
|
|
212
|
+
- `systemd-unit/cloudflared.service`: the systemd unit per the vendor
|
|
213
|
+
as-a-service docs.
|
|
214
|
+
- `<runtime>/cloudflare/tunnels/public-hostname.yaml`: manifest without
|
|
215
|
+
the AUD block.
|
|
216
|
+
- `<runtime>/cloudflare/tunnels/access-gated.yaml`: manifest with the
|
|
217
|
+
AUD block on every non-catch-all rule.
|
|
218
|
+
- `<runtime>/credentials/probe.json.example`: placeholder credentials
|
|
219
|
+
documenting the 0o400 mode discipline.
|
|
220
|
+
- `sidecars/access-gated.applied.json`: `appliedCapabilities` includes
|
|
221
|
+
`zeroTrustGate`.
|
|
222
|
+
- `sidecars/public-hostname.applied.json`: `appliedCapabilities`
|
|
223
|
+
excludes `zeroTrustGate`.
|
|
224
|
+
|
|
225
|
+
Reviewer boot (mocked, no account) from the fixture directory:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
cd packages/rcf-lite/test/fixtures/hetzner-throwaway-server
|
|
229
|
+
node ./run-tunnel-manifest-schema-validate.mjs && node ./run-cloudflared-config-lint.mjs && node ./run-aud-presence-check.mjs
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Reviewer boot (real account, throwaway server): set
|
|
233
|
+
`CI_HAS_CLOUDFLARE_ACCOUNT=true` and `CI_HAS_HETZNER_ACCOUNT=true`
|
|
234
|
+
(and `CI_HAS_CLOUDFLARE_ACCESS=true` for the gated sub-case) with the
|
|
235
|
+
account tokens loaded via `security-secrets-management`, then invoke the
|
|
236
|
+
two `run-real-account-*.mjs` scripts.
|
|
237
|
+
|
|
238
|
+
## Composition on the shelf
|
|
239
|
+
|
|
240
|
+
Consumes:
|
|
241
|
+
- `platform-docker-compose-host` v1.0.0 (`containerHost` capability) for
|
|
242
|
+
the compose-service runtime path.
|
|
243
|
+
- `deploy-hetzner-server` v1.0.0 (`cloudHost` capability) for the bare
|
|
244
|
+
systemd-unit runtime path.
|
|
245
|
+
- `security-secrets-management` v1.0.1 (`secretsProvider` capability) for
|
|
246
|
+
the tunnel credentials `secretRef`.
|
|
247
|
+
- Optionally `edge-cloudflare-access` v1.0.0 (`zeroTrustGate` capability)
|
|
248
|
+
for the Access-gated shape.
|
|
249
|
+
|
|
250
|
+
Provides:
|
|
251
|
+
- `tunnelBridge` (declared under `capabilities` on `blueprint.json`).
|
|
252
|
+
- New global topic `edgeIngressBridge` on ADR-4001 (scope global).
|
|
253
|
+
|
|
254
|
+
Conflict-by-design with future non-Cloudflare siblings on the
|
|
255
|
+
`edgeIngressBridge` topic: Tailscale Funnel, Fly.io proxy, etc.
|
|
256
|
+
would contribute the same topic string with a different mechanism.
|
|
257
|
+
Round 7 ships only the Cloudflare Tunnel provider.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "edge-cloudflare-tunnel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"category": "edge",
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"tunnelBridge"
|
|
7
|
+
],
|
|
8
|
+
"suggestedCompanions": [
|
|
9
|
+
{
|
|
10
|
+
"role": "logging",
|
|
11
|
+
"reason": "Every cloudflaredReady, tunnelConnectorUp, tunnelConnectorDown, ingressRuleReloaded and accessGateRefused event writes through the applied logger; a logging companion supplies the factory."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"role": "errorHandling",
|
|
15
|
+
"reason": "A cloudflared exit, a JWT-check failure, an ingress-rule schema violation, a hostname-DNS-route API refusal constructs an internal error record; an error-handling companion supplies the record factory and the boundary."
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"elicits": [
|
|
19
|
+
{
|
|
20
|
+
"id": "tunnel-name",
|
|
21
|
+
"prompt": "The Cloudflare Tunnel name (also the manifest filename under cloudflare/tunnels/<name>.yaml). Names the connector in cloudflared tunnel list and in the routing DNS entries.",
|
|
22
|
+
"kind": "string",
|
|
23
|
+
"default": "probe"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "cloudflare-zone-secret",
|
|
27
|
+
"prompt": "security-secrets-management reference to the Cloudflare zone the tunnel serves under. Read at apply time to create the DNS route; never inlined into the manifest.",
|
|
28
|
+
"kind": "string",
|
|
29
|
+
"default": "hq-estate/CLOUDFLARE_ZONE_ID"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "public-hostname-template",
|
|
33
|
+
"prompt": "Template for the public hostname (e.g. <service>.<zone>). The template is expanded per ingress rule at apply time.",
|
|
34
|
+
"kind": "string",
|
|
35
|
+
"default": "<service>.<zone>"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "ingress-rules",
|
|
39
|
+
"prompt": "Ingress rules array: each entry names a public hostname and the internal service URL to route to. The last entry is a mandatory catch-all http_status 404 (added by the blueprint if omitted).",
|
|
40
|
+
"kind": "string",
|
|
41
|
+
"default": "[]"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "access-aud",
|
|
45
|
+
"prompt": "Optional Access AUD tag for the Access-gated shape. When present AND edge-cloudflare-access is applied (zeroTrustGate on appliedCapabilities), every ingress rule attaches originRequest.access.aud with this value. When absent OR edge-cloudflare-access is not applied, the block is omitted and the tunnel serves in public-hostname mode.",
|
|
46
|
+
"kind": "string",
|
|
47
|
+
"default": ""
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "connector-runtime",
|
|
51
|
+
"prompt": "Connector runtime per ADR-4002. compose-service is the shipped default when containerHost is applied (T-2 platform-docker-compose-host on top of T-1 deploy-hetzner-server); systemd-unit is the alternative for a bare cloudHost per the cloudflared as-a-service local-configuration-file docs.",
|
|
52
|
+
"kind": "enum",
|
|
53
|
+
"options": [
|
|
54
|
+
"compose-service",
|
|
55
|
+
"systemd-unit"
|
|
56
|
+
],
|
|
57
|
+
"default": "compose-service"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"contributions": [
|
|
61
|
+
{
|
|
62
|
+
"id": "edge-cloudflare-tunnel-REQ-001",
|
|
63
|
+
"kind": "req",
|
|
64
|
+
"path": "requirements/edge-cloudflare-tunnel-req-001.json"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "edge-cloudflare-tunnel-REQ-002",
|
|
68
|
+
"kind": "req",
|
|
69
|
+
"path": "requirements/edge-cloudflare-tunnel-req-002.json"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "edge-cloudflare-tunnel-REQ-003",
|
|
73
|
+
"kind": "req",
|
|
74
|
+
"path": "requirements/edge-cloudflare-tunnel-req-003.json"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "edge-cloudflare-tunnel-REQ-004",
|
|
78
|
+
"kind": "req",
|
|
79
|
+
"path": "requirements/edge-cloudflare-tunnel-req-004.json"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "edge-cloudflare-tunnel-REQ-005",
|
|
83
|
+
"kind": "req",
|
|
84
|
+
"path": "requirements/edge-cloudflare-tunnel-req-005.json"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "edge-cloudflare-tunnel-US-39101",
|
|
88
|
+
"kind": "us",
|
|
89
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39101.json"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "edge-cloudflare-tunnel-US-39102",
|
|
93
|
+
"kind": "us",
|
|
94
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39102.json"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "edge-cloudflare-tunnel-US-39103",
|
|
98
|
+
"kind": "us",
|
|
99
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39103.json"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "edge-cloudflare-tunnel-US-39104",
|
|
103
|
+
"kind": "us",
|
|
104
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39104.json"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "edge-cloudflare-tunnel-US-39105",
|
|
108
|
+
"kind": "us",
|
|
109
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39105.json"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "edge-cloudflare-tunnel-US-39106",
|
|
113
|
+
"kind": "us",
|
|
114
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39106.json"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "edge-cloudflare-tunnel-US-39107",
|
|
118
|
+
"kind": "us",
|
|
119
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39107.json"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "edge-cloudflare-tunnel-US-39108",
|
|
123
|
+
"kind": "us",
|
|
124
|
+
"path": "user-stories/edge-cloudflare-tunnel-us-39108.json"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "TAC-4001-edge-cloudflare-tunnel-connector-shape",
|
|
128
|
+
"kind": "tac",
|
|
129
|
+
"path": "tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "TAC-4002-edge-cloudflare-tunnel-manifest-schema",
|
|
133
|
+
"kind": "tac",
|
|
134
|
+
"path": "tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "TAC-4003-edge-cloudflare-tunnel-access-binding",
|
|
138
|
+
"kind": "tac",
|
|
139
|
+
"path": "tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "ADR-4001-edge-cloudflare-tunnel-ingress-bridge-contract",
|
|
143
|
+
"kind": "adr",
|
|
144
|
+
"path": "adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json",
|
|
145
|
+
"scope": "global",
|
|
146
|
+
"topic": "edgeIngressBridge",
|
|
147
|
+
"recommendedDefault": true,
|
|
148
|
+
"elicited": false,
|
|
149
|
+
"standardsTraceClause": "Cloudflare Tunnel overview and get-started documented mechanics per hetzner-round-7-spec-2026-09-07.md"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "ADR-4002-edge-cloudflare-tunnel-connector-runtime",
|
|
153
|
+
"kind": "adr",
|
|
154
|
+
"path": "adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json",
|
|
155
|
+
"recommendedDefault": true,
|
|
156
|
+
"elicited": true,
|
|
157
|
+
"standardsTraceClause": "Cloudflare cloudflared as-a-service documented systemd unit per hetzner-round-7-spec-2026-09-07.md"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "ADR-4003-edge-cloudflare-tunnel-hostname-mode",
|
|
161
|
+
"kind": "adr",
|
|
162
|
+
"path": "adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json",
|
|
163
|
+
"recommendedDefault": true,
|
|
164
|
+
"elicited": false,
|
|
165
|
+
"standardsTraceClause": "Cloudflare Access self-hosted-public-app documented shape per hetzner-round-7-spec-2026-09-07.md"
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-4001-edge-cloudflare-tunnel-ingress-bridge-contract",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Edge ingress bridge contract: Cloudflare Tunnel as the shipped ingress-bridge shape from a cloudHost service to the CF edge (scope global, new topic edgeIngressBridge)",
|
|
8
|
+
"context": "An applying project running on a Linux cloudHost needs one shipped answer to the question of how a service on that host reaches the public internet without opening origin ports. Round-7 T-3 mints the edgeIngressBridge topic on this ADR. Cloudflare Tunnel is the shipped v1.0.0 mechanism (cloudflared connector out to the CF edge; DNS route on a Cloudflare zone; ingress rules mapping public hostname to internal service URL). Every applied ingress bridge needs one project-wide answer to the edgeIngressBridge topic; a future non-Cloudflare sibling (Tailscale Funnel, Fly.io proxy) would contribute the same topic string with a different mechanism, forcing a DELIBERATE conflict the operator resolves per the round-2 conflict pattern. Standards trace clause: Cloudflare Tunnel overview and get-started documented mechanics per hetzner-round-7-spec-2026-09-07.md.",
|
|
9
|
+
"decision": "The shipped v1.0.0 default is a Cloudflare Tunnel connector as the edgeIngressBridge provider. A cloudflared connector on the applying host (compose service by default when containerHost is applied per ADR-4002; systemd unit on a bare cloudHost) reads a tunnel manifest at cloudflare/tunnels/<name>.yaml and terminates ingress at the CF edge. The edgeIngressBridge topic scope is global on this blueprint; the topic string is edgeIngressBridge (lower camelCase, one concept per topic, no version suffix, per the round-2 rules-for-new-topics clause on the shelf registry). Standards trace clause: Cloudflare Tunnel overview and get-started documented mechanics per hetzner-round-7-spec-2026-09-07.md.",
|
|
10
|
+
"consequences": "Consumer blueprints that expose a service publicly declare capabilities: [tunnelBridge] on their applied contract to know a bridge is in place. Composition with zeroTrustGate (edge-cloudflare-access) toggles the Access-gated ingress shape per ADR-4003. A T-3-only project (no reverse proxy) declares no 80/443 firewall rule; the tunnel is the sole path from the edge to the origin. Rebuild-from-manifest is always possible: the manifest is the source of truth, the running connector is derivative. Folding this shape into edge-cloudflare-access was rejected: Access is an authorisation gate, Tunnel is an ingress bridge; the responsibilities are distinct and one applies without the other.",
|
|
11
|
+
"alternativesConsidered": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Fold into edge-cloudflare-access as an adapter",
|
|
14
|
+
"reason": "Access is an authorisation gate on top of an ingress; Tunnel is an ingress bridge. One applies without the other. Folding conflated two decisions the operator ships separately. Rejected per Baz ruling decision 21."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "Ship a Tailscale Funnel or Fly.io proxy sibling in the same PR",
|
|
18
|
+
"reason": "Out of scope for round 7. Siblings mint on operator demand into their own reserved slots and conflict-by-design with this blueprint on the edgeIngressBridge topic."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
22
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-4002-edge-cloudflare-tunnel-connector-runtime",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Connector runtime: compose-service default when containerHost applied; systemd-unit alternative on bare cloudHost",
|
|
8
|
+
"context": "The cloudflared connector runs in one of two supported vendor shapes: as a container image published on the cloudflared downloads page, or as a systemd unit installed via the vendor package per the cloudflared as-a-service local-configuration-file docs. An applying project running its stack under docker compose has a natural home for the connector alongside its other services (same network, same logging driver, same restart policy). An applying project running bare on the host has no compose stack and needs a systemd unit for boot ordering and journald logging. Standards trace clause: Cloudflare cloudflared as-a-service documented systemd unit per hetzner-round-7-spec-2026-09-07.md.",
|
|
9
|
+
"decision": "The connector runtime is elicited via connector-runtime with enum {compose-service, systemd-unit} and recommendedDefault compose-service. When the containerHost capability is applied (T-2 platform-docker-compose-host) the shipped default is compose-service; when the host runs bare (only T-1 applied) the operator picks systemd-unit at apply time. Standards trace clause: Cloudflare cloudflared as-a-service documented systemd unit per hetzner-round-7-spec-2026-09-07.md.",
|
|
10
|
+
"consequences": "The fixture ships both variants and both pass the local probes (manifest-schema-validate, cloudflared-config-lint, aud-presence-check). A future non-Cloudflare tunnel sibling (Tailscale Funnel, Fly.io proxy) reuses the runtime enum shape but the elicit lives on that sibling own ADR (conflict-by-design on edgeIngressBridge per ADR-4001). The systemd variant carries an install caveat in the guide: the vendor package landed via apt or curl per the as-a-service docs, which is out of scope for the blueprint itself.",
|
|
11
|
+
"alternativesConsidered": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Ship only the compose-service variant",
|
|
14
|
+
"reason": "A bare cloudHost is a valid apply shape; forcing docker compose onto a host that does not want it is a hard cost the elicit avoids."
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
18
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-4003-edge-cloudflare-tunnel-hostname-mode",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Hostname mode: access-gated when zeroTrustGate applied (ingress attaches AUD); public-hostname when absent (block omitted)",
|
|
8
|
+
"context": "A tunnel can serve a public hostname openly (any request reaches the origin, protected only by whatever authentication the origin itself carries) or Access-gated (cloudflared refuses at the edge any request without a valid Access JWT for a named AUD, per the vendor self-hosted-public-app docs). The hostname mode is not elicited: it is DISCOVERED from the applied capability set. When zeroTrustGate is applied (edge-cloudflare-access v1.0.0), the tunnel binds the Access AUD; when it is not, the tunnel serves in public-hostname mode. Standards trace clause: Cloudflare Access self-hosted-public-app documented shape per hetzner-round-7-spec-2026-09-07.md.",
|
|
9
|
+
"decision": "ADR-4003 records the hostname mode as elicited: false and discovered from applied capabilities via the sidecar rcf/blueprints/edge-cloudflare-tunnel.applied.json. When appliedCapabilities includes zeroTrustGate, every ingress rule under the tunnel manifest attaches an originRequest.access block naming the AUD; when it does not, the block is omitted. The aud-presence-check probe runs on both fixture variants and asserts the shape flips correctly. Standards trace clause: Cloudflare Access self-hosted-public-app documented shape per hetzner-round-7-spec-2026-09-07.md.",
|
|
10
|
+
"consequences": "A project that applies zeroTrustGate later inherits the Access-gated shape on the next reapply of edge-cloudflare-tunnel without a manual manifest edit; a project that removes zeroTrustGate drops to public-hostname mode explicitly. A silent drift (the sidecar keeps zeroTrustGate but the ingress loses the AUD block) is caught by aud-presence-check refusing before the shape ships. Fixture variants live under cloudflared/sidecars/{access-gated,public-hostname}.applied.json; the SIMULATE_AUD_DROP mutation on the gated fixture proves the refuse path.",
|
|
11
|
+
"alternativesConsidered": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Elicit the hostname mode as a separate parameter",
|
|
14
|
+
"reason": "The applied capability set is already the source of truth; a separate elicit would drift from that set. Discovery keeps the shape aligned by construction."
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
18
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
19
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Probe: aud-presence-check.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId list:
|
|
4
|
+
// - AC-tunnel-accessGated (primary)
|
|
5
|
+
// - AC-tunnel-accessGatedRefuse (drift refuse)
|
|
6
|
+
// accountBound: false.
|
|
7
|
+
//
|
|
8
|
+
// The probe reads the two sidecars under fixtureRoot()/sidecars and the
|
|
9
|
+
// two manifests per runtime under fixtureRoot()/<runtime>/cloudflare/tunnels/<mode>.yaml.
|
|
10
|
+
// A fixture-side delegate points FIXTURE_ROOT at a scratch copy to drive
|
|
11
|
+
// the fixture-side aud-drop mutation. The probe module itself is a pure
|
|
12
|
+
// function of the files it sees.
|
|
13
|
+
//
|
|
14
|
+
// Passes when:
|
|
15
|
+
// - Each access-gated manifest carries originRequest.access.aud equal to
|
|
16
|
+
// the gated sidecar accessAud on every non-catch-all ingress rule.
|
|
17
|
+
// - Each public-hostname manifest carries no originRequest.access block.
|
|
18
|
+
//
|
|
19
|
+
// Fails when the gated variant drifts to omit the AUD block (the drift-
|
|
20
|
+
// refuse case). Also fails when the public-hostname variant carries an
|
|
21
|
+
// AUD block (that shape has no source of authority).
|
|
22
|
+
|
|
23
|
+
import { readFile } from 'node:fs/promises';
|
|
24
|
+
import { resolve } from 'node:path';
|
|
25
|
+
import { runShim, fixtureRoot, parseTunnelYaml, RUNTIMES } from './probe-utils.mjs';
|
|
26
|
+
|
|
27
|
+
export const anchorAcIds = ['AC-tunnel-accessGated', 'AC-tunnel-accessGatedRefuse'];
|
|
28
|
+
export const accountBound = false;
|
|
29
|
+
|
|
30
|
+
async function loadSidecar(root, name) {
|
|
31
|
+
const path = resolve(root, 'sidecars', `${name}.applied.json`);
|
|
32
|
+
const text = await readFile(path, 'utf8');
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadManifest(root, runtime, mode) {
|
|
37
|
+
const path = resolve(root, runtime, 'cloudflare/tunnels', `${mode}.yaml`);
|
|
38
|
+
const text = await readFile(path, 'utf8');
|
|
39
|
+
return parseTunnelYaml(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function runProbe() {
|
|
43
|
+
const results = [];
|
|
44
|
+
const extra = { fixtureRoot: fixtureRoot() };
|
|
45
|
+
const root = fixtureRoot();
|
|
46
|
+
const gatedSidecar = await loadSidecar(root, 'access-gated');
|
|
47
|
+
const publicSidecar = await loadSidecar(root, 'public-hostname');
|
|
48
|
+
const gatedHasZtg = (gatedSidecar.appliedCapabilities ?? []).includes('zeroTrustGate');
|
|
49
|
+
const publicHasZtg = (publicSidecar.appliedCapabilities ?? []).includes('zeroTrustGate');
|
|
50
|
+
extra.gatedSidecarZtg = gatedHasZtg;
|
|
51
|
+
extra.publicSidecarZtg = publicHasZtg;
|
|
52
|
+
if (!gatedHasZtg) {
|
|
53
|
+
results.push({
|
|
54
|
+
anchorAcId: 'AC-tunnel-accessGated',
|
|
55
|
+
verdict: 'fail',
|
|
56
|
+
detail: `access-gated sidecar is missing zeroTrustGate in appliedCapabilities (found: ${(gatedSidecar.appliedCapabilities ?? []).join(', ')})`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (publicHasZtg) {
|
|
60
|
+
results.push({
|
|
61
|
+
anchorAcId: 'AC-tunnel-accessGated',
|
|
62
|
+
verdict: 'fail',
|
|
63
|
+
detail: `public-hostname sidecar unexpectedly names zeroTrustGate in appliedCapabilities (found: ${(publicSidecar.appliedCapabilities ?? []).join(', ')})`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const runtime of RUNTIMES) {
|
|
67
|
+
// access-gated manifest: every non-catch-all rule attaches the AUD.
|
|
68
|
+
const gDoc = await loadManifest(root, runtime, 'access-gated');
|
|
69
|
+
const gRules = Array.isArray(gDoc.ingress) ? gDoc.ingress : [];
|
|
70
|
+
const gNonCatch = gRules.filter((r) => !String(r.service ?? '').startsWith('http_status:'));
|
|
71
|
+
const gAttached = gNonCatch.filter((r) => r?.originRequest?.access?.aud === gatedSidecar.accessAud);
|
|
72
|
+
if (gAttached.length === gNonCatch.length && gNonCatch.length > 0) {
|
|
73
|
+
results.push({
|
|
74
|
+
anchorAcId: 'AC-tunnel-accessGated',
|
|
75
|
+
verdict: 'pass',
|
|
76
|
+
detail: `${runtime}/access-gated: all ${gNonCatch.length} non-catch-all rule(s) attach originRequest.access.aud=${gatedSidecar.accessAud}`,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
const missing = gNonCatch
|
|
80
|
+
.filter((r) => r?.originRequest?.access?.aud !== gatedSidecar.accessAud)
|
|
81
|
+
.map((r, i) => `service=${r.service} aud=${r?.originRequest?.access?.aud ?? '(none)'}`);
|
|
82
|
+
results.push({
|
|
83
|
+
anchorAcId: 'AC-tunnel-accessGatedRefuse',
|
|
84
|
+
verdict: 'fail',
|
|
85
|
+
detail: `${runtime}/access-gated: sidecar declares zeroTrustGate + accessAud=${gatedSidecar.accessAud} but ${gNonCatch.length - gAttached.length}/${gNonCatch.length} rule(s) drift (missing or wrong AUD). Drifted: ${missing.slice(0, 3).join(' | ')}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// public-hostname manifest: no rule carries originRequest.access.
|
|
89
|
+
const pDoc = await loadManifest(root, runtime, 'public-hostname');
|
|
90
|
+
const pRules = Array.isArray(pDoc.ingress) ? pDoc.ingress : [];
|
|
91
|
+
const carriers = pRules
|
|
92
|
+
.map((r, i) => ({ i, r }))
|
|
93
|
+
.filter(({ r }) => r?.originRequest?.access);
|
|
94
|
+
if (carriers.length === 0) {
|
|
95
|
+
results.push({
|
|
96
|
+
anchorAcId: 'AC-tunnel-accessGated',
|
|
97
|
+
verdict: 'pass',
|
|
98
|
+
detail: `${runtime}/public-hostname: manifest omits originRequest.access on every ingress rule (open ingress by design)`,
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
results.push({
|
|
102
|
+
anchorAcId: 'AC-tunnel-accessGated',
|
|
103
|
+
verdict: 'fail',
|
|
104
|
+
detail: `${runtime}/public-hostname: sidecar declares no zeroTrustGate but ${carriers.length} ingress rule(s) carry originRequest.access; that shape has no source of authority`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { results, extra };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const engine = { kind: 'sidecar-read', image: 'edge-cloudflare-tunnel aud-presence facade', healthy: true };
|
|
112
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
113
|
+
await runShim('aud-presence-check', engine, runProbe);
|
|
114
|
+
}
|