Publish the annia webhook through the Cloudflare Tunnel #88

Closed
opened 2026-09-22 14:03:39 -07:00 by tepichord · 1 comment
Owner
   One entry in `cloudflare/main.tf`'s `webhook_routes`, so Forgejo can deliver its
   webhook to annia's enqueuer. Everything else in this root — the tunnel, the DNS
   records, the WAF rules — already generalises over the list.

   ```hcl
   {
     hostname = "annia.vando.dev"
     path     = "/api/hook"
     service  = "http://annia-enqueuer.annia.svc.cluster.local:80"
   },
   ```

   - `annia.vando.dev` satisfies the `webhook_hostnames_live_in_the_zone` check (apex or
     exactly one level below it) and gets its proxied CNAME automatically from the
     existing `for_each` over hostnames. No DNS work.
   - **The service string is a contract with the enqueuer issue:** that Service must be
     named `annia-enqueuer`, live in annia's namespace, and answer on port 80. Renaming it
     later is a two-place change.

   ## Why the path is `/api/hook`

   `local.hook_paths` is `distinct()` over every route's path, and it already contains
   `/api/hook` (Woodpecker) and `/hook/` (Flux). A third route carrying `/api/hook` adds no
   path, so **the WAF expression is unchanged** — the rate-limit rules and their assertions
   stay byte-identical, and `can(regex("/hook", ...))` stays literally true for
   `/api/hook`.

   The alternative — publishing a new path such as `/enqueue` — would force the test's
   invariant to be *restated* rather than re-indexed ("every published route must be a hook
   path"), and that invariant exists because a count-only check would not catch a route that
   exposes an app's UI.

   What this costs: two hostnames now publish the same path, so the **host** is what tells
   them apart, exactly as the file already argues for telling apps apart by hostname rather
   than by path. cloudflared matches rules in order and first-match wins, but the hostnames
   are distinct, so the order of these entries is not load-bearing.

   ## What moves in the tests

   `cloudflare/tests/plan.tftest.hcl`, run `ingress_publishes_only_the_hook_path` — the
   catch-all shifts, and every assertion that reads "the routes" grows by one:

   | Assertion | From | To |
   |---|---|---|
   | `length(ingress)` | `3` | `4` ("three webhook routes plus the required catch-all") |
   | hook-path `alltrue` loop | `slice(..., 0, 2)` | `slice(..., 0, 3)` |
   | distinct hostnames | `== 2` | `== 3` (same new slice) |
   | catch-all location | `ingress[2]` | `ingress[3]` |
   | new: `ingress[2]` path and service | — | `/api/hook`, the enqueuer Service |

   The `ingress[0]` and `ingress[1]` assertions (Woodpecker, Flux) stay exactly as they
   are, so the file still reads as "the two known routes, then the new one".

   Two runs that must **not** be touched, because they are what proves the design:

   - `rate_limit_rule_is_free_plan_legal` asserts the whole expression literally
     (`contains "/api/hook" or contains "/hook/"`). If it fails, the `distinct()` claim was
     wrong and the path choice needs revisiting.
   - `tests/test_gitops.py::test_flux_receiver_is_published_through_the_tunnel` reads the
     set of published paths out of `main.tf`. That set stays `{/api/hook, /hook/}`, and
     neither nests inside the other, so the test passes unchanged.

   `hostnames_outside_the_zone_are_rejected` and `deeper_hostnames_are_rejected` set their
   own `webhook_routes`, so they are unaffected.

   ```bash
   terraform -chdir=cloudflare test        # offline, mock providers
   ```

   ## The apply

   ```bash
   terraform -chdir=cloudflare plan        # review
   # then apply — with the user's explicit confirmation, per this repo's rule
   ```

   This root reads the cluster kubeconfig from the control-plane state, so `control-plane/`
   must have been applied at least once.

   ## Behaviour to record, because both failures read as something else

   - **The WAF is not authentication.** The tunnel adds POST-only and rate limiting on the
     path; the enqueuer itself must verify Forgejo's HMAC over the raw request body.
   - **A route that was never added answers `404` at the Cloudflare edge** and never reaches
     the cluster. The symptom is the forge reporting a failed delivery with nothing in the
     pod logs — which reads as a broken receiver, not as a missing route.
   - **A route whose Service does not exist answers `502`.** That is the useful split: `404`
     means this issue, `502` means the enqueuer issue.

   ## Documentation

   - **`cloudflare/README.md`** — the zone-shape table gains a `annia.vando.dev` row
     (proxied, served by this cluster, Cloudflare Universal SSL), and the route list plus
     the "Adding a webhook endpoint" example reflect three routes.
   - **`OPERATIONS.md` §1** — the existing "Public webhook endpoints" row gains
     `annia.vando.dev/api/hook`.
   - **`ARCHITECTURE.md` §7** — the public-surface sentences and the traffic table (see the
     last note in this issue).

   ## Out of scope

   The enqueuer itself and the RabbitMQ objects it publishes to — that is the enqueuer
   issue, which this route only points at.

   ## Done when

   From outside the tailnet:

   ```bash
   # The rewrite is live at the edge (a TLS error here means Universal SSL has not issued yet):
   openssl s_client -connect annia.vando.dev:443 -servername annia.vando.dev </dev/null 2>/dev/null |
     openssl x509 -noout -subject -issuer -dates

   # An unsigned POST reaches the enqueuer and is rejected by the app, not by the edge:
   curl -sS -X POST -w '\nhttp=%{http_code}\n' https://annia.vando.dev/api/hook
   # expect: the enqueuer's own rejection (HMAC), not 404 and not 502

   # Negative control — the app's UI is not reachable on the hook hostname:
   curl -sS -o /dev/null -w '%{http_code}\n' https://annia.vando.dev/
   # expect: 404
   ```

   And the docs above are updated, with `terraform -chdir=cloudflare test` green.
````markdown One entry in `cloudflare/main.tf`'s `webhook_routes`, so Forgejo can deliver its webhook to annia's enqueuer. Everything else in this root — the tunnel, the DNS records, the WAF rules — already generalises over the list. ```hcl { hostname = "annia.vando.dev" path = "/api/hook" service = "http://annia-enqueuer.annia.svc.cluster.local:80" }, ``` - `annia.vando.dev` satisfies the `webhook_hostnames_live_in_the_zone` check (apex or exactly one level below it) and gets its proxied CNAME automatically from the existing `for_each` over hostnames. No DNS work. - **The service string is a contract with the enqueuer issue:** that Service must be named `annia-enqueuer`, live in annia's namespace, and answer on port 80. Renaming it later is a two-place change. ## Why the path is `/api/hook` `local.hook_paths` is `distinct()` over every route's path, and it already contains `/api/hook` (Woodpecker) and `/hook/` (Flux). A third route carrying `/api/hook` adds no path, so **the WAF expression is unchanged** — the rate-limit rules and their assertions stay byte-identical, and `can(regex("/hook", ...))` stays literally true for `/api/hook`. The alternative — publishing a new path such as `/enqueue` — would force the test's invariant to be *restated* rather than re-indexed ("every published route must be a hook path"), and that invariant exists because a count-only check would not catch a route that exposes an app's UI. What this costs: two hostnames now publish the same path, so the **host** is what tells them apart, exactly as the file already argues for telling apps apart by hostname rather than by path. cloudflared matches rules in order and first-match wins, but the hostnames are distinct, so the order of these entries is not load-bearing. ## What moves in the tests `cloudflare/tests/plan.tftest.hcl`, run `ingress_publishes_only_the_hook_path` — the catch-all shifts, and every assertion that reads "the routes" grows by one: | Assertion | From | To | |---|---|---| | `length(ingress)` | `3` | `4` ("three webhook routes plus the required catch-all") | | hook-path `alltrue` loop | `slice(..., 0, 2)` | `slice(..., 0, 3)` | | distinct hostnames | `== 2` | `== 3` (same new slice) | | catch-all location | `ingress[2]` | `ingress[3]` | | new: `ingress[2]` path and service | — | `/api/hook`, the enqueuer Service | The `ingress[0]` and `ingress[1]` assertions (Woodpecker, Flux) stay exactly as they are, so the file still reads as "the two known routes, then the new one". Two runs that must **not** be touched, because they are what proves the design: - `rate_limit_rule_is_free_plan_legal` asserts the whole expression literally (`contains "/api/hook" or contains "/hook/"`). If it fails, the `distinct()` claim was wrong and the path choice needs revisiting. - `tests/test_gitops.py::test_flux_receiver_is_published_through_the_tunnel` reads the set of published paths out of `main.tf`. That set stays `{/api/hook, /hook/}`, and neither nests inside the other, so the test passes unchanged. `hostnames_outside_the_zone_are_rejected` and `deeper_hostnames_are_rejected` set their own `webhook_routes`, so they are unaffected. ```bash terraform -chdir=cloudflare test # offline, mock providers ``` ## The apply ```bash terraform -chdir=cloudflare plan # review # then apply — with the user's explicit confirmation, per this repo's rule ``` This root reads the cluster kubeconfig from the control-plane state, so `control-plane/` must have been applied at least once. ## Behaviour to record, because both failures read as something else - **The WAF is not authentication.** The tunnel adds POST-only and rate limiting on the path; the enqueuer itself must verify Forgejo's HMAC over the raw request body. - **A route that was never added answers `404` at the Cloudflare edge** and never reaches the cluster. The symptom is the forge reporting a failed delivery with nothing in the pod logs — which reads as a broken receiver, not as a missing route. - **A route whose Service does not exist answers `502`.** That is the useful split: `404` means this issue, `502` means the enqueuer issue. ## Documentation - **`cloudflare/README.md`** — the zone-shape table gains a `annia.vando.dev` row (proxied, served by this cluster, Cloudflare Universal SSL), and the route list plus the "Adding a webhook endpoint" example reflect three routes. - **`OPERATIONS.md` §1** — the existing "Public webhook endpoints" row gains `annia.vando.dev/api/hook`. - **`ARCHITECTURE.md` §7** — the public-surface sentences and the traffic table (see the last note in this issue). ## Out of scope The enqueuer itself and the RabbitMQ objects it publishes to — that is the enqueuer issue, which this route only points at. ## Done when From outside the tailnet: ```bash # The rewrite is live at the edge (a TLS error here means Universal SSL has not issued yet): openssl s_client -connect annia.vando.dev:443 -servername annia.vando.dev </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates # An unsigned POST reaches the enqueuer and is rejected by the app, not by the edge: curl -sS -X POST -w '\nhttp=%{http_code}\n' https://annia.vando.dev/api/hook # expect: the enqueuer's own rejection (HMAC), not 404 and not 502 # Negative control — the app's UI is not reachable on the hook hostname: curl -sS -o /dev/null -w '%{http_code}\n' https://annia.vando.dev/ # expect: 404 ``` And the docs above are updated, with `terraform -chdir=cloudflare test` green. ````
tepichord added this to the MVP project 2026-09-22 14:03:39 -07:00
Author
Owner

Naina:

Overview

Publish annia.vando.dev/api/hook through the existing Cloudflare Tunnel so Forgejo can deliver webhooks to annia's enqueuer. The change is one entry in cloudflare/main.tf's webhook_routes; DNS, the tunnel ingress, and the WAF rules all derive from that list, so nothing else in the root moves. local.hook_paths already contains /api/hook (Woodpecker)1, so the WAF expression stays byte-identical.

Technical Approach

  1. Add the route to cloudflare/main.tf's webhook_routes default

    {
      hostname = "annia.vando.dev"
      path     = "/api/hook"
      service  = "http://annia-enqueuer.annia.svc.cluster.local:80"
    },
    

    The Service name/namespace/port are a contract with #89 — renaming is a two-place change.2

  2. Re-index run "ingress_publishes_only_the_hook_path" in cloudflare/tests/plan.tftest.hcl3

    Assertion From To
    length(...config.ingress) 3 4 ("three webhook routes plus the required catch-all")
    hook-path alltrue loop slice(..., 0, 2) slice(..., 0, 3)
    distinct hostnames == 2 == 3 (same new slice)
    catch-all location ingress[2] ingress[3]
  3. Add the new-route assertion after the Flux pair

    assert {
      condition     = cloudflare_zero_trust_tunnel_cloudflared_config.milner.config.ingress[2].path == "/api/hook"
      error_message = "the annia enqueuer is published on /api/hook (annia's own hostname tells it apart from Woodpecker)"
    }
    
    assert {
      condition = cloudflare_zero_trust_tunnel_cloudflared_config.milner.config.ingress[2].service == (
        "http://annia-enqueuer.annia.svc.cluster.local:80"
      )
      error_message = "the route must terminate on the annia enqueuer Service"
    }
    
  4. Leave these two untouched — they are the proof the design holds

    • rate_limit_rule_is_free_plan_legal asserts the literal expression; if it fails, distinct() did not absorb the new path.45
    • tests/test_gitops.py::test_flux_receiver_is_published_through_the_tunnel reads paths from main.tf; the set stays {/api/hook, /hook/}.6
  5. Update the three documentation surfaces789

    • cloudflare/README.md — add annia.vando.dev row to the zone-shape table; extend the route list and "Adding a webhook endpoint" example to three routes.
    • OPERATIONS.md §1 — Public webhook endpoints row gains `annia.vando.dev/api/hook`.
    • ARCHITECTURE.md §7 — turn the Woodpecker-only sentences/rows (lines ~250, ~306, ~327) into generic "webhook endpoints" covering ci/flux/annia.
  6. Verify and apply

    terraform -chdir=cloudflare test     # offline, mock providers
    terraform -chdir=cloudflare plan     # review; needs control-plane/ applied for kubeconfig
    # apply only with the user's explicit confirmation, per this repo's rule
    

Implementation Details

  • Files to modify: cloudflare/main.tf, cloudflare/tests/plan.tftest.hcl, cloudflare/README.md, OPERATIONS.md, ARCHITECTURE.md.
  • No new dependency, no new resource: DNS (cloudflare_dns_record.webhook), ingress (..._config.milner), and local.hook_path_expression all iterate over var.webhook_routes.10
  • Constraint: cloudflared cannot rewrite paths, so /api/hook is a hard contract with the enqueuer (§Research Findings).11
  • Constraint: annia.vando.dev is exactly one level below vando.dev, satisfying check "webhook_hostnames_live_in_the_zone".12
  • Out-of-scope notes:
    • #25's plan chose hostname annia.vando.dev with path /enqueue and queue annia-wakeups; this issue supersedes both — the enqueuer serves /api/hook and publishes to annia.dispatch.
    • The enqueuer Service/manifests, HMAC verification, and RabbitMQ topology objects belong to #89.
    • A hostname never added has no DNS record, so its symptom is NXDOMAIN rather than the issue's "404 at the edge"; the 404-vs-502 split still holds for a route whose path or Service is wrong.

Acceptance Criteria

  • var.webhook_routes default contains the annia.vando.dev / /api/hook / annia-enqueuer entry.
  • terraform -chdir=cloudflare test is green, with the four index/slice assertions re-based and the new ingress[2] assertions added.
  • rate_limit_rule_is_free_plan_legal and test_flux_receiver_is_published_through_the_tunnel pass unmodified.
  • terraform -chdir=cloudflare plan shows exactly the new proxied CNAME and ingress entry, and no WAF rule diff.
  • Zone-shape table, OPERATIONS.md §1, and ARCHITECTURE.md §7 all list annia.vando.dev/api/hook.
  • Apply is performed only after explicit user confirmation.

Testing Plan

  • terraform -chdir=cloudflare test — offline plan tests, mock providers.
  • run "ingress_publishes_only_the_hook_path" — 4 ingress entries, catch-all last, all published paths match /hook, 3 distinct hostnames.
  • rate_limit_rule_is_free_plan_legal — unchanged literal expression over two distinct paths.
  • tests/test_gitops.py::test_flux_receiver_is_published_through_the_tunnel — unchanged.
  • Post-apply, from outside the tailnet:
    curl -sS -X POST -w '\nhttp=%{http_code}\n' https://annia.vando.dev/api/hook  # enqueuer HMAC rejection, not 404/502
    curl -sS -o /dev/null -w '%{http_code}\n' https://annia.vando.dev/             # 404 (UI not exposed)
    
  • milner #89 — "Create the annia enqueuer": fixes Service name/port and the /api/hook path this route targets.
  • milner #25 — worker pod plan; its /enqueue route and annia-wakeups queue name are superseded here.
  • milner #87 — annia namespace ownership.
  • tepichord/annia-infra #308 — the pointer contract the enqueuer produces.

Research Findings

  • cloudflared rejects a service URL carrying a path, so the route path reaches the enqueuer verbatim — /api/hook is a hard cross-issue contract.
  • Cloudflare evaluates ingress rules top-to-bottom, requires host and path to both match, and requires a final catch-all; annia's and Woodpecker's distinct hostnames make entry order non-load-bearing.
  • Free-plan WAF allows 1 rate-limit rule (Path-only expressions) and 5 custom rules — confirming /api/hook traffic is covered with no new rule.
  • Universal SSL covers only the apex plus first-level subdomains, matching the webhook_hostnames_live_in_the_zone guardrail that annia.vando.dev satisfies.
  • Tunnel troubleshooting: 502 means the tunnel is up but the origin is unreachable (wrong Service/port), supporting the issue's "502 means the enqueuer issue" diagnostic.

Pre-Mortem

  1. The route is never applied, so the webhook stays unpublished — the plan defers apply to the user's explicit confirmation, and this environment has no cloudflare/terraform.tfvars, no .env Cloudflare token, and no control-plane/terraform.tfstate, so the pipeline can only run the mocked terraform test. Nothing creates the DNS record, Forgejo keeps failing to deliver to annia.vando.dev/api/hook as NXDOMAIN rather than the plan's predicted 404/502, and every acceptance box still reads green.
  2. The route points at an origin that does not exist yet — http://annia-enqueuer.annia.svc.cluster.local:80 is only a contract with #89, which is still in planning with no Deployment, no image pipeline, and one already-superseded spelling (/enqueue in #25). If #89 lands with a different Service name, namespace, port or path — or lands after this route is live — the endpoint answers 502 and, because cloudflared cannot rewrite paths, the correction is another infrastructure pull request.
  3. The positional ingress assertions are re-based by hand and can go unvalidated — the plan rewrites four index/slice bounds (length == 4, two slice(..., 0, 3), distinct == 3, ingress[3]) and adds ingress[2] claims that encode list order rather than route identity, so an entry added anywhere but last contradicts them silently. cloudflare/tests/plan.tftest.hcl only executes when cloudflare/.terraform exists, and the CI test step never runs terraform init, so a miscounted index can pass the gate meant to catch it.
Naina: ## Overview Publish `annia.vando.dev/api/hook` through the existing Cloudflare Tunnel so Forgejo can deliver webhooks to annia's enqueuer. The change is one entry in `cloudflare/main.tf`'s `webhook_routes`; DNS, the tunnel ingress, and the WAF rules all derive from that list, so nothing else in the root moves. `local.hook_paths` already contains `/api/hook` (Woodpecker)<sup>[1](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/cloudflare/main.tf?display=source#L164)</sup>, so the WAF expression stays byte-identical. ## Technical Approach 1. **Add the route to `cloudflare/main.tf`'s `webhook_routes` default** ```hcl { hostname = "annia.vando.dev" path = "/api/hook" service = "http://annia-enqueuer.annia.svc.cluster.local:80" }, ``` The Service name/namespace/port are a contract with #89 — renaming is a two-place change.<sup>[2](https://git.gloom.garden/tepichord/milner/issues/89)</sup> 2. **Re-index `run "ingress_publishes_only_the_hook_path"` in `cloudflare/tests/plan.tftest.hcl`**<sup>[3](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/cloudflare/tests/plan.tftest.hcl?display=source#L53)</sup> | Assertion | From | To | |---|---|---| | `length(...config.ingress)` | `3` | `4` (`"three webhook routes plus the required catch-all"`) | | hook-path `alltrue` loop | `slice(..., 0, 2)` | `slice(..., 0, 3)` | | distinct hostnames | `== 2` | `== 3` (same new slice) | | catch-all location | `ingress[2]` | `ingress[3]` | 3. **Add the new-route assertion after the Flux pair** ```hcl assert { condition = cloudflare_zero_trust_tunnel_cloudflared_config.milner.config.ingress[2].path == "/api/hook" error_message = "the annia enqueuer is published on /api/hook (annia's own hostname tells it apart from Woodpecker)" } assert { condition = cloudflare_zero_trust_tunnel_cloudflared_config.milner.config.ingress[2].service == ( "http://annia-enqueuer.annia.svc.cluster.local:80" ) error_message = "the route must terminate on the annia enqueuer Service" } ``` 4. **Leave these two untouched — they are the proof the design holds** - `rate_limit_rule_is_free_plan_legal` asserts the literal expression; if it fails, `distinct()` did not absorb the new path.<sup>[4](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/cloudflare/tests/plan.tftest.hcl?display=source#L238)</sup><sup>[5](https://developers.cloudflare.com/waf/rate-limiting-rules/)</sup> - `tests/test_gitops.py::test_flux_receiver_is_published_through_the_tunnel` reads paths from `main.tf`; the set stays `{/api/hook, /hook/}`.<sup>[6](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/tests/test_gitops.py?display=source#L1594)</sup> 5. **Update the three documentation surfaces**<sup>[7](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/cloudflare/README.md?display=source#L16)</sup><sup>[8](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/OPERATIONS.md?display=source#L45)</sup><sup>[9](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/ARCHITECTURE.md?display=source#L250)</sup> - `cloudflare/README.md` — add `annia.vando.dev` row to the zone-shape table; extend the route list and "Adding a webhook endpoint" example to three routes. - `OPERATIONS.md` §1 — `Public webhook endpoints` row gains `` `annia.vando.dev/api/hook` ``. - `ARCHITECTURE.md` §7 — turn the Woodpecker-only sentences/rows (lines ~250, ~306, ~327) into generic "webhook endpoints" covering `ci`/`flux`/`annia`. 6. **Verify and apply** ```bash terraform -chdir=cloudflare test # offline, mock providers terraform -chdir=cloudflare plan # review; needs control-plane/ applied for kubeconfig # apply only with the user's explicit confirmation, per this repo's rule ``` ## Implementation Details - **Files to modify**: `cloudflare/main.tf`, `cloudflare/tests/plan.tftest.hcl`, `cloudflare/README.md`, `OPERATIONS.md`, `ARCHITECTURE.md`. - **No new dependency, no new resource**: DNS (`cloudflare_dns_record.webhook`), ingress (`..._config.milner`), and `local.hook_path_expression` all iterate over `var.webhook_routes`.<sup>[10](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/cloudflare/main.tf?display=source#L234)</sup> - **Constraint**: cloudflared cannot rewrite paths, so `/api/hook` is a hard contract with the enqueuer (§Research Findings).<sup>[11](https://github.com/cloudflare/cloudflared/blob/9e94122d2b876c8b89e4632f2f6d4b2f7efcb989/ingress/ingress.go#L305)</sup> - **Constraint**: `annia.vando.dev` is exactly one level below `vando.dev`, satisfying `check "webhook_hostnames_live_in_the_zone"`.<sup>[12](https://git.gloom.garden/tepichord/milner/src/commit/354ba0bf8fb52de95c086e0e28d10e8a4e3a8882/cloudflare/main.tf?display=source#L343)</sup> - **Out-of-scope notes**: - #25's plan chose hostname `annia.vando.dev` with path `/enqueue` and queue `annia-wakeups`; this issue supersedes both — the enqueuer serves `/api/hook` and publishes to `annia.dispatch`. - The enqueuer Service/manifests, HMAC verification, and RabbitMQ topology objects belong to #89. - A hostname never added has no DNS record, so its symptom is NXDOMAIN rather than the issue's "404 at the edge"; the 404-vs-502 split still holds for a route whose path or Service is wrong. ## Acceptance Criteria - [ ] `var.webhook_routes` default contains the `annia.vando.dev` / `/api/hook` / `annia-enqueuer` entry. - [ ] `terraform -chdir=cloudflare test` is green, with the four index/slice assertions re-based and the new `ingress[2]` assertions added. - [ ] `rate_limit_rule_is_free_plan_legal` and `test_flux_receiver_is_published_through_the_tunnel` pass unmodified. - [ ] `terraform -chdir=cloudflare plan` shows exactly the new proxied CNAME and ingress entry, and no WAF rule diff. - [ ] Zone-shape table, `OPERATIONS.md` §1, and `ARCHITECTURE.md` §7 all list `annia.vando.dev/api/hook`. - [ ] Apply is performed only after explicit user confirmation. ## Testing Plan - `terraform -chdir=cloudflare test` — offline plan tests, mock providers. - `run "ingress_publishes_only_the_hook_path"` — 4 ingress entries, catch-all last, all published paths match `/hook`, 3 distinct hostnames. - `rate_limit_rule_is_free_plan_legal` — unchanged literal expression over two distinct paths. - `tests/test_gitops.py::test_flux_receiver_is_published_through_the_tunnel` — unchanged. - Post-apply, from outside the tailnet: ```bash curl -sS -X POST -w '\nhttp=%{http_code}\n' https://annia.vando.dev/api/hook # enqueuer HMAC rejection, not 404/502 curl -sS -o /dev/null -w '%{http_code}\n' https://annia.vando.dev/ # 404 (UI not exposed) ``` ## Related Issues/PRs - milner #89 — "Create the annia enqueuer": fixes Service name/port and the `/api/hook` path this route targets. - milner #25 — worker pod plan; its `/enqueue` route and `annia-wakeups` queue name are superseded here. - milner #87 — `annia` namespace ownership. - tepichord/annia-infra #308 — the pointer contract the enqueuer produces. ## Research Findings - cloudflared rejects a `service` URL carrying a path, so the route path reaches the enqueuer verbatim — `/api/hook` is a hard cross-issue contract. - Cloudflare evaluates ingress rules top-to-bottom, requires host and path to both match, and requires a final catch-all; annia's and Woodpecker's distinct hostnames make entry order non-load-bearing. - Free-plan WAF allows 1 rate-limit rule (Path-only expressions) and 5 custom rules — confirming `/api/hook` traffic is covered with no new rule. - Universal SSL covers only the apex plus first-level subdomains, matching the `webhook_hostnames_live_in_the_zone` guardrail that `annia.vando.dev` satisfies. - Tunnel troubleshooting: 502 means the tunnel is up but the origin is unreachable (wrong Service/port), supporting the issue's "502 means the enqueuer issue" diagnostic. ## Pre-Mortem 1. **The route is never applied, so the webhook stays unpublished** — the plan defers `apply` to the user's explicit confirmation, and this environment has no `cloudflare/terraform.tfvars`, no `.env` Cloudflare token, and no `control-plane/terraform.tfstate`, so the pipeline can only run the mocked `terraform test`. Nothing creates the DNS record, Forgejo keeps failing to deliver to `annia.vando.dev/api/hook` as NXDOMAIN rather than the plan's predicted 404/502, and every acceptance box still reads green. 2. **The route points at an origin that does not exist yet** — `http://annia-enqueuer.annia.svc.cluster.local:80` is only a contract with #89, which is still in planning with no Deployment, no image pipeline, and one already-superseded spelling (`/enqueue` in #25). If #89 lands with a different Service name, namespace, port or path — or lands after this route is live — the endpoint answers 502 and, because cloudflared cannot rewrite paths, the correction is another infrastructure pull request. 3. **The positional ingress assertions are re-based by hand and can go unvalidated** — the plan rewrites four index/slice bounds (`length == 4`, two `slice(..., 0, 3)`, `distinct == 3`, `ingress[3]`) and adds `ingress[2]` claims that encode list order rather than route identity, so an entry added anywhere but last contradicts them silently. `cloudflare/tests/plan.tftest.hcl` only executes when `cloudflare/.terraform` exists, and the CI `test` step never runs `terraform init`, so a miscounted index can pass the gate meant to catch it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tepichord/milner#88
No description provided.