Skip to content

Edges serve stale WAF config for an unmeasured length of time #266

Closed

Description

@ecv

Summary

TrafficProtectionPolicy spec changes made on the upstream control plane take an unbounded, unobservable amount of time to reach the edge data plane. During that window an edge serves the stale (previously-valid-or-invalid) WAF config, with no signal — on the cluster or in metrics — that a given edge is behind the control plane for a given policy.

This surfaced while investigating #242: a tenant corrected an inverted-paranoia TPP upstream, but the edge continued serving the old inverted config (which 500s 100% of the route via CRS 901500 fail-closed) for a window before converging.

Evidence

Observed on prod us-central-1-alice, policy ns-c5f1cf4e…/kev1n-proxy:

Source generation paranoiaLevels
upstream (karmada control plane) 8 {detection: 2, blocking: 2} (valid — tenant-corrected)
edge us-central-1-alice (first read) 7 {detection: 1, blocking: 2} (inverted — 500s all traffic)
edge us-central-1-alice (later read) 8 {detection: 2, blocking: 2} (converged)

So the projection is eventually consistent — it did converge — but the edge served the broken inverted config for the lag window, and nothing exposed that the edge was a generation behind. This is not a stuck/broken sync; it's propagation latency with zero visibility.

Why it matters

Asks

  1. Observability: expose the applied TPP generation / resourceVersion per edge (metric or status), so control-plane vs edge skew is queryable. A spec.observedGeneration-style signal on the edge-side attachment, or a metric labeled by policy + edge, would let us alert on lag.
  2. Bound / quantify the latency: measure typical and worst-case upstream→edge propagation time for a TPP spec change; decide whether it needs an SLO.
  3. (stretch) Consider surfacing staleness at the control plane so a tenant/operator sees "not yet applied on N edges" after an edit.

Not this issue

Related: #242, #255.

Activity

  1. ecv commented on Jul 14, 2026

    @ecv
    ContributorAuthor

    Proposed regression-coverage acceptance

    Propagation-staleness is invisible today and the parity gate (#225) only reads a single point in time. With the prod-fidelity env live (#275/#276) and policy delivery landing in #227, attach explicit coverage:

    • A test/e2e/ scenario applies a TPP change upstream, then asserts the edge converges to the new generation and that convergence is observable — a spec.observedGeneration-style signal or per-edge/per-policy metric — not inferred from a request eventually succeeding.
    • Scenario exercises the failure-sensitive path from TrafficProtectionPolicy Enforce mode: 500 with truncated/empty body on all traffic #242: correct an inverted-paranoia TPP upstream and assert the edge stops 500ing once (and only once) it reports the corrected generation.
    • Guard asserts control-plane-vs-edge skew is queryable during the lag window (the observability ask in this issue), so an unbounded silent tail fails the test.

    Depends on the observability signal from this issue's Ask #1 existing first — the test asserts against it.

  2. mattdjenkinson commented on Aug 4, 2026

    @mattdjenkinson
    Contributor

    Implementation in progress across NSO + infra + portal + datumctl for tenant-visible WAF readiness.

    What lands

    • API: ancestor Programmed on TrafficProtectionPolicy (keep Accepted = attach-only). True only when all edges have applied the current generation; message carries M/N. Reasons: Programmed / Pending / PartialFailure.
    • Edge feedback: extension server stamps Programmed on the edge-local TPP after a successful WAF apply, plus nso_extension_tpp_{cache,applied}_generation gauges and generation in datum-gateway route metadata.
    • Karmada: staging-first AND statusAggregation for TPP (apps/.../federated/tpp-aggregation, wired via clusters/staging/infrastructure/network-services-operator-tpp-aggregation.yaml).
    • Portal: Protection shows Protected (Enforce∧Programmed), Monitoring (Observe∧Programmed), or Pending while converging.
    • CLI: CRD printcolumns + datumctl conditions view flattens status.ancestors[].conditions.

    Explicit follow-ups

    • Probe-proved blocking (Protecting / synthetic 403) — deferred (cloud-portal#1336 family).
    • Promote TPP aggregation to production after staging soak.
    • infra#3740: retarget waf-configuration e2e to gate on Programmed.
  3. mattdjenkinson commented on Aug 5, 2026

    @mattdjenkinson
    Contributor

    Ask #2 (measure → SLO): samples justify a 30s budget.

    Env Policy Converged
    staging nowaf-test ~1.2s
    staging help-d9c5g5 ~1.6s
    prod portfolio-lmpn0u ~3.3s (skew visible ~0.7s)
    prod waf-test-3gsu2v ~2.1s

    Alert PR: #337 — TrafficProtectionPolicyApplyLagSLOViolation when nso_extension_tpp_cache_generation stays ahead of nso_extension_tpp_applied_generation for 30s (~10× measured lag).

  4. ecv commented on Aug 10, 2026

    @ecv
    ContributorAuthor

    Went through #331 and #335 against what this issue actually asked for, and checked prod rather than trusting the release notes. Leaving this open — a good chunk landed, but not the part the title is about.

    What is genuinely done, and verified live: every TrafficProtectionPolicy now carries an ancestor Programmed condition that the edge extension server stamps only after a successful WAF apply, Karmada AND-aggregates it across members with M/N edges programmed generation N in the message, and the upstream reconciler mirrors it back with Pending / PartialFailure / Programmed. The aggregation ResourceInterpreterCustomization is applied and healthy on the prod Karmada hub, all seventeen edge clusters are on v0.25.2, and nso_extension_tpp_cache_generation / nso_extension_tpp_applied_generation are reporting per edge and per policy across the whole fleet. #335 makes idle proxies backfill the condition instead of waiting for someone to touch the spec. That covers Ask #1 in its status form, and it covers the stretch Ask #3 — a tenant can now see "not yet applied on N edges" after an edit.

    What is not done is Ask #2, and the gap is more than "the alert PR hasn't merged yet".

    The measurement happened and 30s was chosen as the budget, but #337 is still open and there is no TrafficProtectionPolicy* alert in prod today, so nothing would page. More importantly, the two gauges the alert watches are both edge-local: cache generation is the generation of the projected object sitting in the edge extension server's informer, and applied generation is what it last pushed into Envoy. The skew between them is the edge-internal translate leg. It is not the control-plane-to-edge leg, which is the leg this issue was opened about — in the original incident the hub was at generation 8 while the edge object was still at 7, and on that edge both gauges would have read 7 with no skew at all. Consistent with that, cache > applied has been flat zero across all seventeen edges for the entire five days since rollout. So the alert as drafted would not have fired for the event that opened this issue.

    The upstream Programmed condition does close that loop, because the controller compares the aggregated observedGeneration against the upstream generation and reports Pending while the edge is behind. But that is a boolean plus a count, not a duration — there is still no series that answers "how long has this policy been behind on this edge", which is what an SLO needs to be measurable rather than just declared. One thing I could not convince myself of and would like a second pair of eyes on: that comparison is between the aggregated observedGeneration reported off the downstream object and the upstream object's generation, and I do not think anything keeps those two generation counters in step, since each apiserver assigns its own. If they can drift the condition could read Pending or Programmed for the wrong reason. I have not reproduced that, so treat it as a question rather than a finding.

    And the half in the title — the edge serving stale config during the window — is untouched. Programmed reports the window, it does not shorten or gate it. A tenant who corrects a broken policy still gets an unbounded tail of the old config being served, and infra#3740 shows the same shape on the create path where enforcement misses the 30s budget roughly one run in three. Nothing shipped changes that behavior.

    The regression-coverage boxes I proposed above are also still unticked. #331 added a Programmed=True assertion to the enforce e2e, which is real progress, but it is a create-path assertion — there is no scenario that changes a TPP upstream and asserts the edge converges to the new generation, none that corrects an inverted-paranoia policy and asserts the 500s stop exactly when the corrected generation is reported, and nothing asserting the skew is queryable during the window.

    Suggested split, if that seems right: keep #266 for the lag half — land #337 or a successor that watches the control-plane-to-edge leg, add a duration signal, and attach the three coverage scenarios — and open a separate issue for the behavioral half, that an edge should not serve stale enforce config for an unbounded window, linked to infra#3740 as the create-path sibling. Happy to be talked out of the split if you would rather keep it as one thread.

    #331
    #335
    #337
    https://github.com/datum-cloud/infra/issues/3740

  5. changed the title [-]Edge serves stale TrafficProtectionPolicy config during upstream→edge propagation with no lag visibility[/-] [+]Edges serve stale WAF config for an unmeasured length of time[/+] on Sep 10, 2026
  6. vvoytovych commented on Sep 30, 2026

    @vvoytovych
    Contributor

    I looked at why a TPP edit can wait for an unrelated change before an edge applies it. This is about the three edges that serve traffic today, on Envoy Gateway v1.7.4 and NSO v0.31.0.

    What I found

    • The retrigger from fix: retrigger EG translation on TPP spec changes #255 cannot start a rebuild.
      • It writes networking.datumapis.com/tpp-generation-<name> on the Gateway (tpp.go).
      • Envoy Gateway re-runs its Gateway API translation for that change (predicates.go). But it copies only gateway.envoyproxy.io/ annotations into its xDS model (listener.go, route.go).
      • So the model comes out equal, Envoy Gateway drops it (map.go), and the xDS stage, where our hook runs, never starts.
      • A translator test shows the model unchanged on Envoy Gateway v1.7.4, v1.8.4, v1.9.2 and main. I can share it.
      • It matches July: the retrigger was already on every edge (v0.24.4 on 07-20), and TPP edits still stayed inert until an unrelated change.
    • The Connector retrigger has the same defect. It writes networking.datumapis.com/connector-ready-generation on the Gateway (retrigger.go). So a Connector going online or offline also waits for an unrelated rebuild. The Programmed backfill uses the TPP annotation (tpp.go), so it waits too.
    • The two lag gauges cannot show a missed rebuild.
    • An edit to a TPP that targets an HTTPRoute has no trigger at all today.
    • A missed rebuild waits for the next one, and that depends on other tenants.
      • On a weekday, rebuilds come in bursts every 15 to 20 minutes.
      • On Sunday 2026-09-27, each edge went more than 3 hours without one.
      • The source is nso_extension_hook_calls_total over both pods of each edge, scraped every 30 s.

    This corrects the report I sent in July, which said the defect "looks already fixed, just not deployed". The fix was deployed, and it cannot work.

    What I plan

    • Keep the cache gauge as what the last hook saw. Add a gauge that the informer sets when a TPP changes, and alert when it is ahead of the cache gauge.
      • That shows a missed rebuild. It does not page on TPPs that never get an applied value.
      • Delete the series when a TPP is deleted.
      • This covers the edge only. The hub-to-edge leg stays with Programmed, as you said in August.
    • Test on a rig at Envoy Gateway v1.7.4. An edit to a Gateway-targeted TPP should start a rebuild through policyResources, and the two retriggers should not.
      • The result decides whether the retriggers can be removed.
      • A Gateway-targeted policy stayed inert in a July test, so this needs a running controller, not only the code.
    • Route-targeted TPPs get their trigger with the edge upgrade to Envoy Gateway v1.9.2 (datum-cloud/infra#6419).
    • An open upstream PR, fix: support ExtensionServerPolicy targeting GatewayClass envoyproxy/gateway#10062, would reject Gateway-targeted extension policies under mergeGateways, and so remove that trigger too.
      • I asked for one release of deprecation (comment).
      • We plan to move the edge TPP copies to route targets, but only after every edge runs v1.9.2.

    Let me know if you have concerns. Thanks.

  7. mattdjenkinson commented on Oct 1, 2026

    @mattdjenkinson
    Contributor

    Hey @vvoytovych this looks good to me, do you want to assign this issue to yourself?

  8. vvoytovych commented on Oct 5, 2026

    @vvoytovych
    Contributor

    Assigned. Thanks @mattdjenkinson

  9. vvoytovych commented on Oct 5, 2026

    @vvoytovych
    Contributor

    I tested this on a running controller: Envoy Gateway v1.9.2 and NSO v0.31.0, with the production config and the us-east-1 objects. For the status tests, Envoy Gateway had only the edge's rights on our API group. We are splitting this issue: the WAF stays here, and the Connector half moves to #451.

    What the test showed

    • A TPP edit starts a rebuild within about 2 seconds, and NSO applies the new generation. That held for Gateway- and route-targeted edits, a retarget and a delete, 16 of 16.
    • The trigger is the TPP's own attachment, through the policyResources setting in infra (helm-release.yaml).
    • Envoy Gateway calls our hook only when its own model changes, and it watches extension objects for spec changes only (controller.go). The other triggers in NSO's design, extensionManager.resources and the two retrigger annotations, never reach the hook (README.md).

    What we fix here, for the WAF

    1. Programmed can report success before every edge has the new generation.
      • Karmada's aggregation counts an edge as programmed for any generation, and reports the highest generation it sees (resourceinterpreter.yaml).
      • We ran it in Karmada's Lua interpreter with one edge at generation 3 and one still at 2. It reports "2/2 edges programmed generation 3", so the upstream check passes (controller).
      • The fix: carry the upstream generation on the edge copy, and have each edge report it. Then count only the edges that report the current one.
    2. A missed rebuild is invisible.
      • The extension server sets both lag gauges inside the hook call (index.go, server.go).
      • The fix: a gauge the informer sets, and an alert when it runs ahead of the cache gauge.
    3. The TPP retrigger is dead code. Its annotation alone started no rebuild, 0 of 5. We remove it.
    4. NSO's own Programmed stamp costs one more full rebuild per edit.
      • Envoy Gateway keeps whole policy objects in its model. So the status write rebuilds everything at its next reconcile (server.go).
      • With the write allowed, the extra rebuild came 8 of 8 times. With it refused, 0 of 4.
      • We fix it together with 1, which changes where Programmed is reported.
    5. If fix: support ExtensionServerPolicy targeting GatewayClass envoyproxy/gateway#10062 lands as written, the edge copies must target HTTPRoutes before we adopt Envoy Gateway v1.10.
    6. The extension server design document gets the real trigger model.

    The Connector half, for #451

    Let me know if you have concerns. Thanks.

  10. vvoytovych commented on Oct 14, 2026

    @vvoytovych
    Contributor

    Closing: a WAF policy edit is now timed to every edge, and a policy that stays behind raises an alert.

    what this issue asked where it is now
    Show when an edge is behind on a policy A policy's Programmed is set per target. It is True only when every edge that holds the policy has built its current generation: #581, #593, datum-cloud/infra#6900, #616. The same changes answer the generation-counter question above (#545).
    Measure how long an edit takes infra's waf-mode-flip switches a live policy to Enforce in every scheduled run and waits for Programmed on the new generation (the wait, datum-cloud/infra#6951). It ran in 7 scheduled production runs from 2026-10-13 16:20Z to 2026-10-14 04:21Z. The edit took 4.3 s at the least, 7.3 s at the median and 19.2 s at the most: chainsaw_test_step_duration_seconds{test="waf-mode-flip",step="flip-to-enforce",environment="prod"}.
    Alert when an edge stays behind TrafficProtectionPolicyNotProgrammedSLOViolation (#620), at warning: a target that is Accepted and not Programmed for 300 s. It replaces #337, whose two gauges stay equal on an edge that misses a rebuild.
    Show the tenant "not yet applied on N edges" In the condition's message since #331 and #335, as noted above.

    The three regression checks proposed above:

    • An edit converges to the new generation, and that is observable. waf-mode-flip above. A True left over from the previous generation never passes it.
      test: Assert TPP Programmed tracks generation bumps #336 asks the same in NSO's own tests.
    • Correcting an inverted-paranoia policy. Admission now rejects an inverted policy (the rule), and infra's waf-inverted-paranoia-reject checks it in every run. So no inverted policy can exist to correct.
    • The lag is visible during the window. The policy reads Programmed False for the new generation until every edge has built it. An edit that never gets there fails waf-mode-flip after 12 minutes.

    An edge still serves the previous config until it builds the new one. That window is now measured and alerted, not removed.

    Follow-ups: #547 removes the two edge gauges, and datum-cloud/infra#6768 alerts when Envoy Gateway fails to turn a change into edge config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions