Skip to content

Mesh 3 policy migration - #7211

Open
justindavies wants to merge 93 commits into
release/mesh-3from
mesh-3-policy-migration
Open

justindavies wants to merge 93 commits into
release/mesh-3from
mesh-3-policy-migration

Conversation

@justindavies

@justindavies justindavies commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Initial commit of Mesh v3 policies

Description

Fixes #7146

Preview Links

Checklist

  • Tested how-to docs. If not, note why here.
  • All pages contain metadata.
  • Any new docs link to existing docs.
  • All autogenerated instructions render correctly (API, decK, Konnect, Kong Manager).
  • Style guide (capitalized gateway entities, placeholder URLs) implemented correctly.
  • Every page has a description entry in frontmatter.
  • Add new pages to the product documentation index (if applicable).

@justindavies
justindavies requested a review from a team as a code owner September 11, 2026 15:25
@netlify

netlify Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for kongdeveloper ready!

Name Link
🔨 Latest commit 4efbdd1
🔍 Latest deploy log https://app.netlify.com/projects/kongdeveloper/deploys/6ac569e7ff4a4f0008311535
😎 Deploy Preview https://deploy-preview-7211--kongdeveloper.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@lmilan
lmilan changed the base branch from main to release/mesh-3 September 14, 2026 08:14
@lmilan lmilan self-assigned this Sep 14, 2026
justindavies and others added 25 commits September 21, 2026 18:08
MeshTrafficPermission was documented as two pages, meshtrafficpermission
and meshtrafficpermission_experimental, both declaring the same policy
name. They described two mutually exclusive spec formats: the 2.x `from`
array with an `action`, and the SPIFFE ID matcher form under `rules`.

Kong Mesh 3 accepts only the second. Verified against a live 3.0 control
plane: applying the `action: Allow` form is rejected with "at least one of
'allow', 'allowWithShadowDeny', 'deny' has to be defined", and `from` was
removed as a policy targetRef concept, with /_resources now reporting
hasFromTargetRef: false for every policy type. So the page labelled
experimental documented the only supported syntax, and the unlabelled one
documented a form the control plane refuses.

There is now a single meshtrafficpermission page covering the SPIFFE ID
form, built from the experimental page's content, plus:

  - the evaluation order, and why a deny cannot be overridden by an allow
  - the spiffeID and sni matchers, with Exact and Prefix, and how a
    prefix addresses a whole namespace
  - allowWithShadowDeny as the way to validate a rule before enforcing it
  - targetRef.sectionName for narrowing to one port
  - zone proxies, carried over from the 2.x page
  - a section on upgrading from 2.x, mapping from/action onto rules

Field names follow the CRD: `spiffeID`, capital ID, as in
api/common/v1alpha1/match.go. A live control plane also accepts
`spiffeId`, since Go's JSON decoder matches field names case
insensitively, but that spelling is not canonical.

Four examples replace the three carried over, adding a shadow-deny one.

The v2 snapshot keeps both 2.x pages, so 2.x readers still have the
`from` documentation. Their canonical_url entries now point at the single
latest page.

The top-level experimental stub is removed, with redirects for its index
and reference URLs. Three example redirects that Fabian added pointing at
the v2 snapshot now point at the migrated examples instead, since those
exist at latest. The legacy service-allow-requests example redirect still
points at v2, because that form is not carried forward.

MeshTrust and MeshIdentity linked to the experimental URL and now link to
the consolidated page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Reach for it when you need to..." addresses the reader as a friend
offering advice. The reader is looking for information about the policy,
not counsel. State what the policy is for instead:

  Use it to trace a failing request across services, ...

Same sentence, same content, no change in meaning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The matcher table claimed both spiffeID and sni take a type of Exact or
Prefix. That holds for spiffeID; sni accepts Exact only.

SNIMatchType is a single-value enum in
api/common/v1alpha1/match.go (+kubebuilder:validation:Enum=Exact), and
ValidateMatch rejects anything else with "unrecognized type, supported
values are: Exact". It also requires the value to be a DNS-1123
subdomain, where spiffeID requires a parseable SPIFFE ID.

The table now carries a type column per matcher rather than one sentence
covering both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The evaluation order was stated correctly but its consequence was not.
In 2.x the last matching rule won, so the common pattern was a mesh-wide
deny-all followed by narrower allow rules that opened specific paths.

That inverts in 3.0. RBACConfigurer.createMatcher emits every deny
matcher from every merged policy before any allow matcher, and Envoy
stops at the first match, so a blanket deny matches all clients and no
later or narrower allow can re-open it. Carrying the 2.x pattern forward
therefore denies everything.

The pattern is also unnecessary: the matcher's OnNoMatch action is
RBAC_DENY, so a mesh with no MeshTrafficPermission already allows
nothing. Only allow rules need writing.

Says so in both places a reader meets the question: under the evaluation
order, and in the section on upgrading from 2.x.

This also confirms the default-deny statement in the page opening, which
had been carried over from the previous page rather than verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The zone proxy section presented 10001 and 10002 as the sectionName
values for zone ingress and egress, carried over from the 2.x page. They
are neither constants nor listener names.

sectionName matches Dataplane.networking.listeners[].name. Per
api/mesh/v1alpha1/dataplane.proto, that field is optional and "if unset,
the port value is used as the name (as a string)". So the value depends
on the listener's port, or on an explicit name where one is set. Neither
number appears anywhere in the Kong Mesh 3 source: the only 10001 is
FirstOutboundPort in a test builder.

10001 and 10002 are the chart defaults
(deployments/charts/kuma/values.yaml, ingress.service.port and
egress.service.port), so they are usually right on a Helm install and
wrong anywhere the ports were changed.

Now explains what sectionName matches, gives the defaults as defaults,
and shows how to read the listener names off the Dataplane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the latest MeshRetry page, with four examples, and points the v2
snapshot's canonical_url entries at it.

Checked against the v3 source rather than carried over from the 2.x page,
which turned up one thing worth a warning on the page:

  retryOn accepts `5xx`, lower case. `5XX` is rejected by the control
  plane as "unknown item '5XX'", because validateHTTPRetryOn switches on
  the constants exactly and All5xx is "5xx". `5XX` nevertheless appears
  as the example value in both the CRD
  (pkg/plugins/policies/meshretry/k8s/crd/kuma.io_meshretries.yaml) and
  the OpenAPI schema, and in the prose of the 2.x page. There is no
  kubebuilder enum on HTTPRetryOn, only an example, so a Kubernetes apply
  passes schema validation and the control plane rejects it afterwards.
  Every other named condition is upper camel case, which makes the
  exception easy to trip over.

Other facts taken from source:

  - targetRef accepts Mesh or Dataplane; to[].targetRef accepts Mesh,
    MeshService, MeshExternalService, MeshMultiZoneService or
    MeshHTTPRoute, per the TopLevelTargetRefKind and
    OutboundTargetRefKind enums.
  - gRPC retryOn is a closed kubebuilder enum, unlike the HTTP list.
  - TCP takes only maxConnectAttempt.
  - Defaults: numRetries 1, backOff.baseInterval 25ms, backOff.maxInterval
    ten times baseInterval, rateLimitedBackOff.maxInterval 300s,
    Predicate.updateFrequency 2, hostSelectionMaxAttempts one reattempt.
  - At least one of tcp, http or grpc is required in a `default`.

The upgrade section records the two 3.0 changes that affect an existing
policy: MeshSubset and MeshServiceSubset are gone as selectors, and
header matches no longer materialize `type: Exact` into the stored
resource, per UPGRADE.md.

Three redirects that pointed latest example URLs at the v2 snapshot now
point at the migrated examples.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every MeshRetry example was rejected by a Kong Mesh 3 control plane. Two
faults, both from copying the shape of the 2.x examples:

  spec.to[0].targetRef.labels (): must be set when kind is MeshService
  spec.to[0].default.http.rateLimitedBackOff.resetHeaders[0].name ():
    in body should match '^[a-z0-9!#$%&'*+\-.^_\x60|~]+$'

OutboundTargetRef carries only kind, labels and sectionName. It has no
name field, so `name: backend` selected nothing and labels is required. A
MeshService is selected the way UPGRADE.md selects one, by
kuma.io/display-name. The 2.x examples used name, namespace, sectionName
and a _port field, none of which apply.

HeaderName is constrained to lower case by a kubebuilder pattern, so
Retry-After and X-RateLimit-Reset had to become retry-after and
x-ratelimit-reset.

Reading the struct was not enough to catch either: the OutboundTargetRef
fields were in front of me and I wrote `name` anyway, and the pattern
lives on the HeaderName type rather than at the point of use. Both
surfaced only on apply.

All four examples/ files and all three policy_yaml blocks in the page now
apply cleanly to a 3.0 control plane. The MeshTrafficPermission page's
four blocks and four examples were re-checked the same way and needed no
change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Two pieces do the work:" and "Three pieces do the work:" personify
configuration fields and tell the reader nothing about what follows.

Replaced with "What each field does:", which names the list.

Three pages: the phrasing originated in the MeshAccessLog overview and
was carried into MeshTrafficPermission and MeshRetry when those were
written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the latest MeshCircuitBreaker page, with four examples, and points
the v2 snapshot's canonical_url entries at it.

Checked against the v3 source, which surfaced two silent-data-loss traps
that both get a warning on the page:

  spec.from is removed, and dropped silently. A policy using from
  alongside to or rules is accepted and the from configuration has no
  effect on inbound traffic; a policy where from was the only field set is
  rejected, because the spec then has neither to nor rules. Per
  UPGRADE.md, from maps onto rules, and a from entry targeting kind: Mesh
  becomes a single catch-all rule.

  healthyPanicThreshold moved here from MeshHealthCheck, and un-migrated
  settings are dropped silently. It now lives at
  to[].default.outlierDetection.healthyPanicThreshold. On Kubernetes the
  old field is pruned by CRD validation and on Universal it is discarded
  during deserialization, so nothing reports it and the cluster reverts to
  Envoy's 50% default.

Other facts taken from source:

  - This policy has both to and rules, unlike MeshRetry (to only) and
    MeshTrafficPermission (rules only). At least one is required, and each
    default needs at least one of connectionLimits or outlierDetection.
  - to[].targetRef accepts Mesh, MeshService, MeshExternalService or
    MeshMultiZoneService. It does not accept MeshHTTPRoute, which MeshRetry
    does: circuit breaking applies to a destination, not to a route.
  - rules applies to all inbound traffic, with no L7 matching, so a single
    catch-all entry is the only available shape.
  - detectors is required whenever outlierDetection is set and needs at
    least one of its five detectors.
  - maxEjectionPercent defaults to 10%, and at least one endpoint is
    always ejectable regardless of it.
  - healthyPanicThreshold defaults to 50%; 0 disables panic mode.

All four examples and all three policy_yaml blocks were applied to a live
Kong Mesh 3 control plane and accepted, then deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A reader coming from MeshTrafficPermission to MeshCircuitBreaker asks why
one matches clients by SPIFFE ID and the other names a MeshService. The
pages carried both facts and explained neither.

Each page now opens its "Where a policy applies" section with the three
selectors and what each answers:

  spec.targetRef        which proxies the policy is installed on
  spec.to[].targetRef   which destination, for traffic the proxy sends
  spec.rules[]          which client, for traffic the proxy receives

Followed by the reason they differ: a destination is something the proxy
chooses to call, so it is named directly; a client is remote and asserts
its own identity, so a name or label would be the caller's own claim about
itself, and it is matched on the SPIFFE ID MeshIdentity issued instead.
That is also why v3 dropped `from` with MeshServiceSubset from
MeshTrafficPermission.

The four shapes differ more than a reader would guess, so each page states
its own rather than implying a common one:

  MeshTrafficPermission  rules only, matching clients
  MeshRetry              to only
  MeshAccessLog          to and rules, rules can match clients
  MeshCircuitBreaker     to and rules, but rules cannot match at all —
                         L7 matching for inbound circuit breaking is not
                         implemented, so it is catch-all only

Kinds per field follow the cross-policy table in UPGRADE.md: only
MeshAccessLog, MeshLoadBalancingStrategy, MeshRetry and MeshTimeout accept
MeshHTTPRoute in to[], which MeshCircuitBreaker rejects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit explained the three selectors on each policy page,
which duplicated a shared concept four times and would have repeated it
across every remaining policy.

Adds /mesh/policy-targeting/, covering:

  - the three selectors and what each answers
  - why outbound names a destination and inbound matches an identity: a
    destination is something the proxy chooses to call, while a client
    describes itself, and a label would be the caller's own claim. This is
    why 3.0 removed `from` and MeshServiceSubset from
    MeshTrafficPermission.
  - what each field accepts, including the cross-policy rules from
    UPGRADE.md: MeshHTTPRoute is accepted in to[] only by MeshAccessLog,
    MeshLoadBalancingStrategy, MeshRetry and MeshTimeout; MeshRateLimit
    and MeshFaultInjection accept Mesh only, and only when the top-level
    targetRef selects a gateway; MeshSubset, MeshServiceSubset and
    MeshGateway are rejected in to[] everywhere.
  - the spiffeID and sni matchers, with sni accepting Exact only.

Each policy page now states only what is true of itself and links to the
reference. The per-policy facts that differ are kept where a reader needs
them: MeshTrafficPermission has no `to`, MeshRetry has no `rules`,
MeshCircuitBreaker allows both but its `rules` cannot match a client.

Placed in app/mesh/ alongside the other standalone mesh references, since
/mesh/policies/ is a generated list page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every policy page now carries "How policies select traffic" as its first
related_resources entry, so the shared concept is the first thing offered
alongside a policy. MeshAccessLog had no related_resources block and now
has one.

Also fixes the tags on the new page: `policies` and `targetref` are not in
the frontmatter schema enum, which failed make validate-frontmatters.
Replaced with `policy`, `service-mesh` and `traffic-control`, which are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"This is the part that looks inconsistent and is not." performs rather
than states. The heading above it already named the subject, so the
sentence carried no information and existed only for effect. Deleted.

Two aphorisms replaced with the mechanism they were standing in for:

  "access control decided on a self-asserted tag was not access control"
  becomes "those selectors matched an inbound client on tags the client
  set itself".

  "sends traffic to all endpoints regardless of health, on the grounds
  that a degraded endpoint beats no endpoint" becomes "resumes sending
  traffic to all endpoints, ejected ones included, rather than failing the
  requests". This also makes healthyPanicThreshold's meaning explicit: the
  percentage of healthy endpoints below which panic mode starts.

"Two differences from other policies are worth knowing" editorialised
about the content and is now "This policy differs from others in two
ways".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the latest MeshTimeout page, with four examples, and points the v2
snapshot's eight canonical_url entries at it. The 2.x page had seven
examples covering variations of the same two cases; the latest page has
four, and the redirects map each old URL onto the one that covers it.

Two restrictions came out of the source and are both confirmed against a
live 3.0 control plane:

  `to` and `rules` are mutually exclusive, unlike MeshCircuitBreaker where
  both may be set together. A policy defining both is rejected with
  "fields 'to' must be empty when 'rules' is defined", so inbound and
  outbound timeouts need separate policies.

  A rule matching on spiffeID may set only http.requestTimeout and
  http.streamIdleTimeout. The other five fields are rejected with "can't
  be specified when matches contain spiffeID because this field cannot be
  conditioned on source identity". The page explains why: those two act on
  a request or stream, which belongs to one client, while
  connectionTimeout, idleTimeout, requestHeadersTimeout, maxStreamDuration
  and maxConnectionDuration act on a connection or listener, which is
  shared.

The defaults are documented, since a mesh already has timeouts and a
policy changes them rather than introducing them. Kuma creates two
MeshTimeout resources per mesh, from pkg/defaults/mesh/meshtimeout.go and
pkg/plugins/policies/core/defaults/consts.go:

  connectionTimeout      outbound 5s    inbound 10s
  idleTimeout            outbound 1h    inbound 2h
  http.requestTimeout    outbound 15s   inbound 0, disabled
  http.streamIdleTimeout outbound 30m   inbound 1h
  http.maxStreamDuration outbound unset inbound 0, disabled

Inbound is twice outbound, so a receiving proxy does not time out a
request before the caller does, and the inbound request timeout is
disabled for the same reason. A mesh created with
skipCreatingInitialPolicies covering MeshTimeout has neither default.

The upgrade section covers `from` becoming `rules`, the removed targetRef
kinds, and the Universal inbound that loses HTTP timeouts when its
protocol is declared only through the kuma.io/protocol tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the latest MeshHealthCheck page, with four examples, and points the
v2 snapshot's canonical_url entries at it.

The healthyPanicThreshold removal is verified live, including the part
that makes it dangerous. A policy setting the field is accepted rather
than rejected, and reading it back shows the field gone:

  $ kongctl create mesh -f hc-with-panic-threshold.yaml
  created
  $ kongctl get mesh meshhealthchecks hc-panic-probe -o json --jq '.spec.to[0].default | keys'
  ["http", "interval", "timeout"]

So nothing reports the loss, and the cluster reverts to Envoy's 50%
default. The page says so, and points at
MeshCircuitBreaker.to[].default.outlierDetection.healthyPanicThreshold.

The page opens by distinguishing this policy from MeshCircuitBreaker,
since they reach the same outcome differently: probes here are scheduled
and independent of real requests, which is active checking, while
outlierDetection judges the traffic already flowing. A destination with
little traffic is the case for active checks, because there may not be
enough requests to judge it by.

Facts taken from source:

  - to only, no rules: a health check is something a caller performs.
    to[].targetRef accepts Mesh, MeshService, MeshExternalService or
    MeshMultiZoneService, and not MeshHTTPRoute, since a probe addresses
    an endpoint rather than a route.
  - At least one of http, tcp or grpc is required.
  - Defaults: interval 1m, timeout 15s, unhealthyThreshold 5,
    healthyThreshold 1, noTrafficInterval 60s, reuseConnection true, HTTP
    path /, expectedStatuses 200 only, and statuses limited to 100-599.
  - Protocol selection takes the most specific configured protocol, and
    disabled: true falls back to the more general one.
  - TCP receive blocks match fuzzily but in order; an empty receive makes
    the probe connect-only.

The page also notes what the default interval and threshold mean together:
an endpoint failing every probe is removed about five minutes after it
starts failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the policy page against the v3 source. The `from` array is gone;
clients are matched with `rules[].matches` on `spiffeID` or `sni`, and
`targetRef.kind` is limited to Mesh and Dataplane.

Document the `to[]` restriction, which is verified in the validator and
against a live v3 control plane: `to` is rejected outright unless
`targetRef.kind` is Mesh, and then only accepts `kind: Mesh`. The
restriction exists because a fault is produced by the proxy answering the
request, so it is configured inbound; the `to` form covers gateways.

Also cover the three fault types with their validated ranges, and the
compounding of percentages down the fault list.

Four examples replace the three v2 ones, all applied to a v3 control
plane and accepted. v2.yml canonical URLs and the latest -> v2 example
redirects now point at the migrated pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the policy page against the v3 source, and cover the behavioural
changes that matter more than the schema ones.

Verified in source and against a live v3 control plane:

- `to[].targetRef` accepts only MeshService, MeshExternalService and
  MeshMultiZoneService. `Mesh` is rejected with `value 'Mesh' is not
  supported`, so a route always names a destination.
- `to[].hostnames` is in the schema but unimplemented, and rejected with
  `must not be defined`. Same for `urlRewrite.hostToBackendHostname`.
- `replacePrefixMatch` is rejected unless every entry in the rule's
  matches uses a PathPrefix path.
- `backendRefs[].kind` no longer accepts MeshServiceSubset, and `tags` is
  gone from the schema.
- A MeshMultiZoneService backendRef requires `port`.
- A PathPrefix other than `/` is rejected if it ends in a slash.

Documented from source, not exercised at runtime: the 404 for a request
matching no rule, the 500 for a rule whose backendRefs all fail to
resolve or name a missing port, the 503 for an all-zero-weight list, and
the Gateway API precedence order from CompareMatch.

`targetRef.kind` is Mesh or Dataplane only; real resources are selected
by labels. Four examples replace the three v2 ones, all applied to a v3
control plane and accepted, along with the page's inline block. v2.yml
canonical URLs and the latest -> v2 example redirects now point at the
migrated pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The section was six warning callouts with no order or reasoning between
them. Replace it with an ordered walkthrough: six steps, each saying what
changed, what breaks if it is skipped, and before/after YAML where the
rewrite is not obvious.

The steps are ordered by what the change costs to miss, and the opening
paragraph draws the distinction that decides that: the selector rewrites
are rejected on apply, while the routing changes pass validation and
change where traffic goes.

Two facts verified live while writing it. A targetRef carrying name and
namespace instead of labels is rejected with `labels (): must be set when
kind is MeshService`, and a legacy top-level kind with `in body should be
one of [Mesh Dataplane]` -- so both are loud, not silent. The 3.x
before/after selector, including `k8s.kuma.io/namespace`, was applied to a
v3 control plane and accepted.

Also dropped UPGRADE.md's claim that MeshServiceSubset survives as a
backendRefs kind. The validator restricts backendRefs to MeshService,
MeshExternalService and MeshMultiZoneService, and a live control plane
rejects MeshServiceSubset there, which agrees with the same document's
other section on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same treatment as MeshHTTPRoute: ordered steps with before/after YAML,
the exact validation error where a change is rejected, and an explicit
note where it is not. The order in each is what the change costs to miss,
so the silent ones come first.

Checking the source rather than the existing text turned up several
things the callout lists had wrong or missing.

A top-level `kind: Dataplane` does not require `labels`. ValidateTargetRef
requires them for MeshService, MeshHTTPRoute, MeshExternalService and
MeshMultiZoneService, and for Dataplane requires nothing, so a 2.x
`kind: Dataplane` with `name` is accepted, `name` is dropped, and the
policy widens from one proxy to every proxy in the mesh. Verified live: a
MeshTrafficPermission written that way was created and read back as a
bare `kind: Dataplane`. That is a security change on MTP and a silent one
everywhere, so it is now a warning on all seven pages.

MeshRetry's note about `type: Exact` no longer being a schema default was
wrong: HeaderMatch.Type still carries `+kubebuilder:default=Exact`.
Replaced with the constraint that actually bites, the lower-case-only
HeaderName pattern, which rejects a 2.x `Retry-After`.

Exact errors and behaviours confirmed against a v3 control plane:

- MTP with only `from`: `spec (): policy must define rules`.
- MeshTimeout, MeshCircuitBreaker, MeshAccessLog with `from` alongside
  `to`/`rules`: created, and `from` absent when read back. With `from`
  alone: `spec (): at least one of 'to' or 'rules' has to be defined`.
- MeshHealthCheck with `healthyPanicThreshold`: created, field absent
  when read back.
- MeshAccessLog with `openTelemetry.endpoint`:
  `openTelemetry.backendRef (): must be defined`. The backendRef form was
  applied and accepted.
- MeshRetry `resetHeaders[].name: Retry-After`: rejected by the
  HeaderName pattern.
- Legacy top-level kinds: `in body should be one of [Mesh Dataplane]`.

Added from source, not previously covered: which MeshTimeout fields sit
under `http` and are lost when a Universal inbound has no protocol
(connectionTimeout and idleTimeout are not); that MeshCircuitBreaker's
to[] does not accept MeshHTTPRoute; the MeshProxyPatch circuitBreakers
merge change; MeshTrafficPermission's MeshIdentity prerequisite, the
legacy TrafficPermission becoming inert, and the three removed control
plane settings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the page against the v3 source, with a migration guide in the
same shape as the other policies.

Verified in source and against a live v3 control plane:

- `to[].targetRef` accepts only MeshService, MeshExternalService and
  MeshMultiZoneService. `Mesh` is rejected, so the 2.x gateway form
  (targetRef: MeshGateway with to[].targetRef.kind: Mesh) has no
  equivalent -- name the destination instead.
- `backendRefs` is required: an empty or missing list is rejected with
  `backendRefs (): must be defined`. This differs from MeshHTTPRoute,
  where omitting it falls back to the destination.
- `to` needs at least one entry, and a `to` entry accepts at most one
  rule, since a TCP rule has no matches to distinguish a second one by.
- backendRefs no longer accept MeshServiceSubset.
- Real resources are selected by labels only.

From the xDS path, not exercised at runtime: an unresolved backendRef is
dropped from the split rather than answered with an error, because a TCP
proxy has no status code to answer with, and where none resolve the
client gets no outbound listener for that destination at all
(generateFromService returns nothing for an empty ref list). That is a
different failure from MeshHTTPRoute's 500.

Also from source: MeshHTTPRoute wins whenever the destination is HTTP
based and any MeshHTTPRoute targets it, whatever either route configures
(getBackendRefs returns nil in that case).

Four examples replace the two v2 ones, all applied to a v3 control plane
and accepted, along with the page's inline block. v2.yml canonical URLs
and the latest -> v2 example redirects now point at the migrated pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the page against the v3 source, with a migration guide in the
same shape as the other policies.

Verified in source and against a live v3 control plane:

- `to[].targetRef.kind: MeshHTTPRoute` accepts only `hashPolicies`.
  `loadBalancer` and `localityAwareness` are rejected with `field is not
  allowed when targetRef.kind is MeshHTTPRoute, only hashPolicies is
  supported`.
- `localityAwareness.crossZone` is rejected on any kind but
  MeshMultiZoneService, with `crossZone is only supported when
  targetRef.kind is MeshMultiZoneService`.
- `affinityTags` weights are all-or-nothing: `all or none affinity tags
  should have weight`.
- A failover `to.type` of Any or None must have no `to.zones`; Only and
  AnyExcept must have some.
- `loadBalancer.ringHash.hashPolicies` is accepted and silently dropped,
  not rejected -- UPGRADE.md left this open ("may be rejected by
  validation or have those fields pruned"). Reading the policy back shows
  an empty `ringHash: {}`, so the algorithm stays and has nothing left to
  hash. That is now the first step of the guide, with a warning, since
  requests that were pinned to an endpoint quietly stop being pinned.

The v2 page documents `hashFunction` as `XX_HASH` / `MURMUR_HASH_2`,
which the control plane rejects: `in body should be one of [XXHash
MurmurHash2]`. The enum has been those two values since at least 2.14, so
this is a docs error rather than an upgrade change, and the new page does
not present it as one.

From source, not exercised at runtime: locality awareness is on unless
disabled (isLocalityAware is true when localityAwareness is unset), and
`localZone`/`crossZone` apply regardless of `disabled`, because
atLeastOneLocalityGroup is evaluated independently of it. Unset
affinityTag weights are generated as 9*10^(n-i-1), which is the 90/9/1
split.

The 2.x Zone Egress caveats are dropped rather than carried over:
ZoneEgress and ZoneIngress are removed in v3, so the restriction they
imposed on the top-level targetRef is gone with them.

Five examples replace the five v2 ones, all applied to a v3 control plane
and accepted, along with the page's inline block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ten policy pages each carried their own "Upgrading from 2.x"
section, which put the migration in front of readers who were there to
configure a policy, and repeated the shared changes ten times. Move the
content to app/mesh/migrate-policies-to-3.md and link to it from each
policy's related resources, second after the targeting page.

Consolidating removed real duplication rather than just relocating it.
Stated once on the new page instead of per policy:

- Legacy top-level targetRef kinds, with the warning that a
  `kind: Dataplane` carrying `name` is accepted and widens the policy to
  the whole mesh. That was repeated on eight pages.
- Real resources selected by labels, and the contrast that the four
  real-resource kinds require labels while Dataplane does not.
- The `from` to `rules` move, now a table of which policies reject a
  stored `from` and which accept and drop it, since that differs.
- The MeshHTTPRoute 404, which reached four other policy pages because
  they can attach to a route.
- The Universal inbound protocol change, which listed a different subset
  of lost L7 filters on each page it appeared on.
- The legacy policies going inert, previously split between the
  MeshTrafficPermission and MeshTCPRoute sections.
- MeshServiceSubset backendRefs and "a route names a destination", shared
  by the two route policies and previously written out twice.

Policy pages drop from 3080 to 2187 lines; the migration page is 585.

No content is lost: every verified error string and behaviour from the
per-policy sections is carried over. Front matter validates, the page
builds, all internal anchors and outbound links resolve, and vale is
clean.

The latest mesh tree has no navigation index yet (only
app/_indices/mesh/v2.yaml exists, and policy-targeting.md is not
registered anywhere either), so the page is reachable by link and will
need adding to the sidebar when that index lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite the page against the v3 source. Upgrade notes go to
/mesh/migrate-policies-to-3/, not onto the policy page.

The `to` and `rules` split is stricter than any other policy, and is
verified live in all four combinations:

- `targetRef: Dataplane` accepts `rules`; `to` is rejected with
  `spec.to (): must not be defined`.
- `targetRef: Mesh` accepts `to`; `rules` is rejected with
  `spec.rules (): must not be defined`.
- Both together: `field 'to' must be empty when 'rules' is defined`.
- Neither: `at least one of 'to' or 'rules' has to be defined`.

That leaves no way to write a mesh-wide inbound limit as `kind: Mesh`.
A bare `kind: Dataplane` with no labels does it instead, which was also
applied and accepted. Both facts are on the page and in the migration
guide, since a 2.x policy carrying both fields has to become two.

Also verified live: a rule whose matches contain a spiffeID cannot carry
`local.tcp` (`can't be specified when matches contain spiffeID because
this field cannot be conditioned on source identity`), because the
identity comes out of the TLS handshake that a connection-level limit is
counting rather than inspecting; and an interval of 10ms is rejected with
`must be greater than: 50ms`.

From source: the limit is a local token bucket enforced per proxy, so a
destination's effective ceiling is the limit times its replica count --
worth stating, since MeshGlobalRateLimit does not exist on v3. The
default rejection status is 429, confirmed by the generated listener
(`status: code: TooManyRequests` when `onRateLimit.status` is unset).

Five examples replace the two v2 ones, all applied to a v3 control plane
and accepted, along with the page's four inline blocks. MeshRateLimit is
added to the shared `from` table on the migration page as one of the
policies that accepts and silently drops a stored `from`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nges

Rewrite the page against the v3 source, with its upgrade notes on
/mesh/migrate-policies-to-3/.

Re-fetching kuma master first turned up sixteen UPGRADE.md sections added
since I extracted it earlier in this work, two of which land on pages
already written:

- `MeshHTTPRoute`/`MeshRetry` header matches and
  `MeshHTTPRoute`/`MeshTCPRoute` backend refs no longer declare schema
  defaults for `type` and `weight`. Both kubebuilder annotations are gone
  from api/common as of today. An earlier commit removed a MeshRetry note
  saying exactly this, on the grounds that the annotation was still
  present -- which was true when I checked and is no longer. Restored as
  a shared note, now covering weight too. Behaviour is unchanged; only
  what the API echoes back differs.
- OpenTelemetry `backendRef` backends always export through kuma-dp, and
  the two settings that made Envoy export directly are removed. Added to
  the MeshAccessLog section, and it applies to MeshTrace and MeshMetric
  when those are written.

MeshPassthrough itself has two changes from commits dated today:

- A `Domain` match is resolved by the sidecar and pinned to a port
  instead of building an ORIGINAL_DST cluster. The old behaviour let a
  workload dial any address, present an allowed domain, and reach that
  address through the policy. A non-wildcard `Domain` now requires
  `port`; a stored match without one stops applying, and where it was the
  policy's only match nothing is left to allow.
- Two matches resolving to the same Envoy filter chain are rejected on
  apply, where such a policy used to be accepted and Envoy then rejected
  the whole listener.

Verified live: domain with `tcp`, partial wildcard, wildcard with an L7
protocol and no port, `mysql` without a port, a duplicate value on one
port and protocol, and an invalid CIDR are all rejected with the strings
quoted on the page. Distinct chains are accepted -- `tcp` and `tls` on
one address, two `http` domains on one port -- as is one domain repeated
across two ports.

The domain-requires-port check could not be exercised: the Konnect
control plane was created 2026-09-07 and the commit landed 2026-09-11, so
it still accepts a portless domain. Documented from source and UPGRADE.md
rather than from the live response.

Also from source, replacing the v2 page's weaker wording: without
transparent proxying the plugin is skipped entirely and the proxy is
given the warning `policy doesn't support proxy running without
transparent-proxy`; and `passthroughMode` defaults to Matched, where the
v2 page says None.

Four examples replace the three v2 ones, all applied to a v3 control
plane and accepted, along with the page's two inline blocks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes.

Each policy page's "Migrate policies to Kong Mesh 3" link now points at
that policy's own section rather than the page root. A deep link that
skipped the shared changes would have been worse than no deep link,
because for some policies the shared changes are all of them, so every
policy section now opens with a line naming the shared changes that apply
to it and linking to each. MeshTimeout had no section of its own for that
reason -- every change affecting it is shared -- so it has one now that
says so and lists them. All twelve deep links resolve against the built
page.

MeshGlobalRateLimit is dropped rather than migrated: it does not exist on
v3. The latest stub is deleted, so /mesh/policies/meshglobalratelimit/
and its generated reference page are gone.

The release map loader takes either `status: pending` or a non-nil
canonical_url, with no state for "removed", so the three v2 entries now
point at the migration page -- which is the successor to those pages, in
that it explains where the policy went. The dead latest URLs redirect to
the removal note.

That note comes from the vendored kong-mesh UPGRADE.md, which already
documents the removal: the policy and its control plane support are gone
along with the rate-limit service the Helm chart deployed, so the
`ratelimit.*` / `global.ratelimit.*` values, the `KMESH_GLOBAL_RATE_LIMIT_*`
env vars and `kmesh.globalRateLimit` all need removing before upgrading.
A leftover resource becomes inert rather than rejected, and the CRD
outlives the upgrade because Helm does not delete CRDs. The note also
says plainly that there is no replacement for a fleet-wide limit, since
MeshRateLimit is local to each proxy.

Re-fetched all three repos first: kuma is at 89cc0192f6 and added no new
UPGRADE.md sections since the last catch-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stubbed provider-schema tests ignore their input, so a regression
that drops the provider pin or the stub mesh resources would keep the
suite green and only drift in warn mode. Add a stub that inspects the
assembled main.tf: validate fails unless it declares the pinned
kong/konnect-beta version, the provider source, and the stub
konnect_mesh and konnect_mesh_control_plane resources.
Escape backslash and double quote in quoted string values, and double a
literal template interpolation start in quoted strings and heredoc
bodies so no published block performs HCL interpolation.
The provider types jsonPatches[].value as a string holding JSON text, so
a scalar patch value now renders as its JSON encoding in quotes on the
Terraform tab. The source example keeps the raw value, so the Kubernetes
and Universal tabs stay unchanged.
The pinned provider models a fixed set of fields (percentage,
standard_deviation_factor, active_request_bias, healthy_panic_threshold,
target_port, and client/overall/random under sampling) as int-or-string
wrapper objects. The Terraform tab now renders them as { integer = ... }
for numeric values and { str = "..." } for strings; other fields keep
their plain rendering.
…ce example

The mis-indented networking key left address as a sibling, which the
validator reports as a gating finding.
The pinned provider has no resource for the legacy ExternalService
policy and types the structured openTelemetry.body as a plain string, so
no valid Terraform block exists for the two v2 external-services
examples and the two logging-to-multiple-backends examples. Set
tools: [kubernetes, universal] on them so the site stops publishing
invalid HCL.
…by default

The default --terraform-validate mode flips from warn to gate; warn stays
as an explicit opt-in. Delete BASELINE.md: the gate re-verifies the
baseline on every run, so a findings-record file holds no remaining
content. Record the standing --skip external-services@v2 (the legacy
ExternalService policy has no top-level spec, so its Kubernetes tab
renders a null spec, a known renderer defect out of scope here) and the
rule that a provider pin bump re-dumps the provider schema before the
gate run. The workflow command stays unchanged; it now gates.
…ecks

The run restructures into three passes (Kubernetes and Universal block
checks, terraform grammar, provider-schema validation), each printing a
start line completed with a done marker and block count when the pass
ends. The provider-schema pass prints no line in off mode. Page HTML is
parsed once and the extracted blocks cached, so no page is read twice.
The findings and summary lines stay unchanged.
Exact-text eq per the design's testing seam, so a wrapper rendered in
the wrong position or with the wrong structure fails the spec, not only
a wrong attribute line.
…ed page

The first full gate run over the overview and other mesh page classes
found three Terraform-tab renderer defects: a source configuration with
top-level labels produced a duplicate labels attribute, HostnameGenerator
(a control-plane-scoped resource) received a mesh attribute the pinned
provider does not define, and a top-level status reached the block
although every konnect_mesh_* resource models it read-only. The renderer
now renders one merged labels attribute, omits the mesh attachment for
control-plane-scoped types, and drops the read-only status.

Also quote the boolean label and tag values in the hostnamegenerator and
performance-tuning examples, drive finding ordinals from the source
instance count, add tests for the no-tag, skip, and ordinal rules, and
trigger the validator workflow for app/mesh changes.
@lmilan lmilan mentioned this pull request Oct 6, 2026
* initial style and structure fixes

* Update application-ingress.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants