Skip to content

Mesh 3 policy migration - #7211

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

justindavies wants to merge 42 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 and others added 30 commits September 10, 2026 15:23
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>
Rewrite the page against the v3 source, with upgrade notes on
/mesh/migrate-policies-to-3/.

The `value` validation claim I first wrote was wrong, and the live control
plane caught it. An unrecognized Envoy field name is accepted, stored in
the policy verbatim, and then dropped when the config is generated --
util_proto.FromJSON sets AllowUnknownFields: true. A misspelled key
therefore passes validation and silently does nothing, while the policy
reads back looking correct. That is now a warning on the page, with the
advice to check a new modification against the proxy's /config_dump
rather than the stored policy. What *is* rejected is a wrong type or enum,
verified live: `bad Duration: time: invalid duration "not-a-duration"` and
`unknown value "NOT_A_REAL_TYPE" for enum
envoy.config.cluster.v3.Cluster.DiscoveryType`.

Other rejections confirmed against the live control plane: two
modifications in one entry, `AddBefore` without `match.name`, `Add` with a
`match`, `Patch` with both `value` and `jsonPatches`, an empty
`appendModifications`, and an operation outside a resource type's set.

The v2 page's origin list is wrong for v3 in both directions. It lists
`gateway`, which no longer exists now the built-in gateway is removed, and
omits `admin` and `proxy-template-modifications`. The new list comes from
the metadata constants, plus the five policy-introduced origins
(`meshaccesslog`, `mesh-trace`, `open-telemetry`, `meshpassthrough`,
`mesh-proxy-patch`) read from their own packages.

I expected `ingress` and `egress` to be dead too, since standalone
ZoneIngress and ZoneEgress proxies are removed, and checked rather than
assuming: ZoneProxyListenerGenerator is still in NewDefaultProxyProfile,
so cross-zone listeners and clusters are still generated with those
origins on ordinary proxies. Both are documented as live, and the
migration note says so explicitly.

One source inconsistency documented as-is: an added cluster is marked
`proxy-template-modifications` (cluster_mod.go) while an added listener is
marked `mesh-proxy-patch` (listener_mod.go). A user matching on origin
would hit that, so the page names both rather than picking one.

Four examples replace the two v2 ones, all applied to a v3 control plane
and accepted, including the hand-written static cluster and virtual host,
which exercises the Envoy proto validation.

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

Verified in source and against a live v3 control plane:

- `tlsVersion` and `tlsCiphers` are accepted only with a top-level
  `targetRef.kind: Mesh`, each rejected on a Dataplane with `<field> can
  only be defined with top level targetRef kind: Mesh`. `mode` is accepted
  on either. The page gives the reason: both peers have to agree on a
  version and cipher, so setting them per workload would let one end
  permit what the other refuses.
- `min` above `max` is rejected with `min version must be lower than
  max`, and `TLSAuto` at either end is not compared.
- A cipher outside the six is rejected, with the error listing them.

From source: an inbound is Strict unless a MeshTLS policy sets otherwise
-- getMeshTLSMode returns ModeStrict for a nil mode, with the comment that
only a MeshTLS policy can select Permissive. Permissive adds a raw-buffer
filter chain beside the TLS one, so it changes the inbound side only and
outbound stays encrypted. The plugin skips a proxy with no workload
identity entirely, logging `skip applying MeshTLS, the proxy has no
workload identity`, so the page says a MeshIdentity is needed first.

`tlsCiphers` sets TlsParameters.CipherSuites, which Envoy applies to TLS
1.2 and below, so the page notes that restricting ciphers while permitting
TLS 1.3 constrains only the older versions.

Two upgrade items, both silent:

- A mesh that relied on `mtls.backends[].mode: PERMISSIVE` gets Strict
  inbounds, because the mode now resolves from the policy alone and
  `Mesh.mtls` is removed from the API. Needs a policy authored before
  upgrading.
- `from` is removed. Unlike the other policies that lost it, MeshTLS has
  no check that the remaining spec configures anything, so a from-only
  policy is *accepted* rather than rejected. Verified live: it applies and
  reads back as nothing but a targetRef, and since an inbound with no
  policy is Strict, a `from` that allowed plaintext silently stops
  allowing it.

Also documented: a permissive inbound cannot identify a plaintext client,
since the identity comes out of the handshake, so a MeshTrafficPermission
matching on spiffeID has nothing to match for that traffic.

Four examples replace the two 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>
Rewrite the page against the v3 source, with upgrade notes on
/mesh/migrate-policies-to-3/.

The one breaking change is the OpenTelemetry backend: the inline
`endpoint` is gone and `backendRef` naming a MeshOpenTelemetryBackend is
the only form. Verified live -- `endpoint` is rejected with
`openTelemetry.backendRef (): must be defined` -- and the backendRef form
was applied and accepted.

Also verified against the live control plane: a Datadog url must use the
http scheme and carry a port and nothing else (`scheme must be http`,
`port must be defined`, `path must not be defined`); a tag setting both
`header` and `literal` is rejected; and a sampling value outside 0-100 is
rejected with `must be between 0 and 100`.

`backends` behaves asymmetrically and the page says so: omitting the field
is rejected with `must be defined`, while an empty list is accepted. Both
confirmed live. Reading the plugin, an empty list makes Configure return
before writing any tracing configuration, so `backends: []` is how a
subset of proxies is exempted from a mesh-wide policy -- and `sampling`
and `tags` alongside it are inert, which the page states rather than
leaving to be discovered.

Dropped a claim I could not support. I had written that the proxy
propagates trace headers whether or not the policy is configured; nothing
in the source says that. Replaced with what the v2 page documents and the
architecture requires: the application has to carry the headers across,
with the six b3 headers named.

Five examples replace the five v2 ones, all applied to a v3 control plane
and accepted, along with the page's four inline blocks. The two v2
"target east/west" examples have no example-level successor -- they
demonstrated MeshSubset targeting, which is gone -- so they point at the
policy page rather than at a misleading match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page opened with "an inbound is Strict unless a MeshTLS policy says
otherwise, so plaintext is rejected by default", and only mentioned the
identity requirement two paragraphs later. Read in order, that tells a
reader with no MeshIdentity the opposite of what happens: per the
kong-mesh upgrade notes, a proxy no MeshIdentity matches gets no mTLS
transport socket at all and serves plaintext, and MeshTLS does not apply
to it.

Reordered so the precondition comes first, and the Strict default is
stated as following from having an identity. Also states plainly that a
mesh wanting mTLS everywhere on Envoy's default version and ciphers needs
no MeshTLS at all, and that MeshIdentity carries no TLS version, cipher
or mode fields, so the two do not overlap.

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

Found a silent security regression that neither UPGRADE.md mentions.
`prometheus.tls.mode: ActiveMTLSBackend` is still in the CRD enum but has
no Go constant and no handling: prometheus_configurer.go applies TLS only
when the mode is ProvidedTLS and the certificate paths are set. Verified
live -- a policy setting ActiveMTLSBackend is accepted -- so a 2.x mesh
that used it keeps applying and serves its metrics endpoint in plaintext,
with nothing reporting the change. It took its certificates from the mesh
CA backend, which v3 removes. Documented as a warning on both the policy
page and the migration guide, with the switch to ProvidedTLS.

That enum value looks like it should have been dropped with the CA
backend, and is worth raising with the Mesh team alongside the other
source inconsistencies collected so far.

Also verified live: the OpenTelemetry inline `endpoint` is rejected with
`openTelemetry.backendRef (): must be defined`; an uncompilable Regex
selector with `invalid regex`; an unknown profile name with `in body
should be one of [All Basic None]`; an out-of-range application port with
`port must be a valid (1-65535)`; a `Contains` selector is accepted; and
more than one backend is accepted, unlike MeshTrace which caps at one.

The validator's selector error says only Regex, Prefix and Exact are
supported while the enum and the switch both accept Contains, so the page
documents the four accepted values rather than quoting that message.

Also on the migration page: MADS no longer starts on Kubernetes control
planes whatever the setting says, so deployments discovering proxies that
way need Prometheus Kubernetes service discovery instead.

Five examples replace the one v2 example, 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>
justindavies and others added 5 commits September 11, 2026 12:18
MeshOPA is enterprise, so the source is in kong-mesh and the upgrade
notes are in the vendored kong-mesh UPGRADE.md rather than Kuma's. That
guide carries four MeshOPA sections the Kuma one does not, which is why
reading both now matters:

- `agentConfig` and `appendPolicies[].rego` move from the removed flat
  DataSource type to SecureDataSource, which needs a `type` discriminator
  and nests the value under a matching field. Verified live: the old flat
  shape is rejected with `rego.type (): in body is required`, and the new
  shape is accepted. `inline` was base64 and `insecureInline.value` is
  plain text, so the note says to decode when rewriting -- otherwise the
  agent gets base64 where it expects rego.
- `targetRef.kind: MeshService` is rejected (verified live).
- `targetRef.name`, `namespace` and `mesh` are pruned rather than
  rejected, so a policy scoped to one service silently widens to every
  proxy matching `kind` and its rego starts evaluating traffic it never
  saw. Now a warning.
- The legacy OPAPolicy resource and CRD are removed.

Verified live besides: an inline rego policy is compiled at write time,
so a syntax error is rejected with the OPA parse error attached; a
statusOnError of 700 is rejected; and a Secret-backed rego is accepted
without being compiled, since the control plane never reads it.

Found a reachable nil dereference in the kong-mesh validator, and did not
trigger it. validator.go:65 dereferences RequestBody.SendRawBody directly
while guarding MaxSize with pointer.Deref, so a `requestBody` with no
`sendRawBody` -- including `requestBody: {}` -- panics inside validation.
There is no panic recovery in pkg/api-server, so I documented it from
source and told the page to set `sendRawBody` explicitly rather than
demonstrating it against a shared control plane.

Also fixes two icons I had invented: meshloadbalancingstrategy.png and
meshopa.png do not exist in app/assets/icons/mesh_policies/. Both v2
pages use policy.svg, which is what these now use. Audited every latest
policy page's icon against the files on disk; the rest resolve.

Five examples replace the one v2 example, all applied to a v3 control
plane and accepted, along with the page's three inline blocks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rces to the render cap

These two are resources rather than policies -- no targetRef, to or
rules, and accepted only in the system namespace on Kubernetes -- so the
pages describe a label selector and issuance rather than traffic
selection, and the migration guide says the shared changes do not apply
to them.

Verified against the live control plane, with names and selectors
neutralised: applying these examples as written would have overwritten
the working `default-identity` and, in the case of `corporate-ca`, won
selection over it (BestMatched ties on match-label count break
lexicographically) and changed the trust domain of every proxy in the
mesh. So each was applied under a zzprobe- name with a selector matching
no workload, which exercises the validator without touching live mTLS.
Validation is what applying verifies; selection is not checked at write
time.

Rejections confirmed live: a Bundled provider with neither autogenerate
nor ca (`ca (): must be defined`); ca alongside autogenerate
(`shouldn't be defined once using autogenerated`); an unparseable
trustDomain template; an unknown provider type; a negative certificate
expiry; a MeshTrust with no caBundles, with a garbage PEM, and with a
PRIVATE KEY block where the certificate belongs -- isPEMCertificate
rejects key blocks by type, so that fails on apply rather than at the
proxy.

Two behaviours found by applying rather than reading:

- A MeshIdentity using the default trust domain template produces one
  MeshTrust per zone, with a different trust domain each
  (default.zone1.mesh.local and default.zone2.mesh.local on this
  two-zone control plane).
- Deleting a MeshIdentity does not delete the MeshTrust it generated. The
  generated resource carries kuma.io/origin: zone, so the Konnect API
  refuses to delete it (`the origin label must be set to 'global'`), with
  or without --force. Now a warning in the migration guide.

Also fixes a silent truncation I had introduced. The layout renders only
four related resources, and adding the migration link pushed ten pages to
five -- MeshTrafficPermission to seven, which was already over the cap
before this work. Each is now trimmed to four. Where plain truncation
would have dropped the more important link, the choice is explicit:
MeshTrafficPermission keeps MeshIdentity and the bundled-provider how-to
rather than losing the prerequisite, and MeshIdentity keeps
MeshTrafficPermission.

Four MeshIdentity examples replace the three v2 ones and two MeshTrust
examples replace the one.

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

Closes the gap the policy migration created: MeshAccessLog, MeshTrace and
MeshMetric all tell the reader to create one of these, and there was no
page to send them to. All three now link to it, as does the migration
guide.

The v2 page is recent and its content holds up against the v3 source --
the env policy, the signal states and the blocked reasons all match
constants in pkg/core/xds/otel_pipe.go and pkg/xds/otel/status. The one
thing that changed is the reference shape: BackendResourceRef in v3
carries `kind` and `labels` only, with labels required, so the v2 page's
guidance about choosing between `backendRef.name` and `backendRef.labels`
(and the hash suffix on synced names) no longer applies. Labels are the
only option, and the page says the oldest matching backend wins, which
makes over-broad labels a creation-order dependency rather than an error.

Verified against a live v3 control plane: an empty spec is accepted, and
is the node-local default where kuma-dp resolves the address from HOST_IP;
`endpoint.path` is rejected with `must not be set when protocol is grpc`,
`must start with /` and `must not contain query or fragment`; a
non-hostname address, an out-of-range port and an unknown `env.mode` are
each rejected with the strings quoted on the page.

Also trimmed: the v2 page ran to 619 lines with five worked examples and a
troubleshooting walkthrough per failure state. This one keeps the field
reference, the environment-variable merge order, the one-backend-per-
signal constraint and the status fields, and drops the repeated examples.

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

The three destination kinds the route and load-balancing pages name
throughout. None is a policy -- each *is* a target, so there is no
targetRef -- and the pages are shaped around that.

Verified against a live v3 control plane:

- MeshService is read only there: a write is refused with `Mesh Service is
  read only on this control plane and cannot be created or updated`,
  because on Kubernetes the control plane generates one per Service. The
  page leads with that, since it changes what a reader does about it. No
  examples were applied for it as a result.
- MeshExternalService: `match.protocol` accepts tcp, grpc, http and http2
  and rejects `tls`, unlike MeshPassthrough; a clientCert without a
  clientKey is rejected with `must be defined when clientCert is
  defined`; a serverName that is not a DNS name is rejected. A full TLS
  origination spec with priorities and SAN matches was accepted.
- MeshMultiZoneService: an empty ports list is rejected with `in body
  should have at least 1 items`, and a label-selected aggregate was
  accepted.

Two things from source worth stating plainly:

- `MeshService.spec.selector.dataplaneTags` was removed in 3.0, and a
  resource carrying only that selector is read back with an empty selector
  matching no proxies. The control plane reports it as a deprecation
  rather than an error, so it is a warning on the page and belongs in the
  migration guide.
- `MeshService.spec.identities[].type: ServiceTag` is gone from the v3
  API, yet the live control plane still emits ServiceTag entries on
  generated MeshServices -- it predates the change. Documented as no
  longer accepted, per the upgrade notes.

MeshService's `tls.status: Pending` gets its own explanation: it exists
because a proxy is certified independently of its destination, so flipping
clients to mTLS in the same pass would drop requests sent before the
destination's inbound chain arrived. A status stuck there is a different
problem from the handover being in progress, which the page says.

Also resolved the four legacy ExternalService entries in the release map.
They were pending against `/mesh/meshexternalservice/`, which now exists;
the two zone-egress examples point there too, ZoneEgress being removed.
That takes _mesh_policies to zero pending entries.

Linked the new pages from MeshLoadBalancingStrategy and MeshPassthrough in
prose, both being at the four-entry related_resources cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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 160ccf8
🔍 Latest deploy log https://app.netlify.com/projects/kongdeveloper/deploys/6aa7c8f049489500083889ca
😎 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.

justindavies and others added 6 commits September 12, 2026 08:35
Replaces the v2 kumactl page. Written against the binary rather than the
implementation: every command and flag shown was run, and a check
confirms all seventeen command forms on the page resolve.

The framing difference from kumactl leads: kongctl reads the resource
types from the control plane instead of carrying a compiled-in list, so
`get mesh resource-types` is authoritative for whatever control plane is
in front of you and a newer policy needs no newer CLI.

Includes a kumactl-to-kongctl mapping table and the behaviours that differ
beyond command names. Two of those are worth calling out because they are
gaps rather than changes:

- There is no `inspect` equivalent. I checked before writing that this was
  a v3 removal and it is not -- the control plane still serves `_policies`
  and `_resources/dataplanes`, and `kumactl inspect` still reaches them.
  So the page says plainly that this is the one case where kumactl still
  does something kongctl cannot, rather than implying the capability is
  gone.
- Installation has no counterpart under `kongctl mesh`. `kongctl install`
  exists but installs kongctl's own features, which the page disambiguates
  so nobody goes looking for a control plane installer.

Documented a flag collision I introduced in the implementation: on `dump
mesh`, `--profile` means the export profile and the global `-p,
--profile` that selects a kongctl configuration profile is unavailable.
Verified both directions -- `dump mesh --profile default` is rejected as
an invalid export profile, and `-p` is not accepted at all. It is a
warning on the page for now; renaming the export flag would be the better
fix and would make that warning deletable.

Verified live: type names resolve as plural, singular or short alias, and
matching is case insensitive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CLI page said inspect was the one case where kumactl still did
something kongctl could not. That is now implemented, so the claim is
removed and the command documented.

Notes the part that differs from kumactl: inspect dataplane reports per
port rather than per proxy, because the control plane only answers with
policy data on the scoped endpoints, and because a policy applying to a
proxy's inbound but not its outbounds is the case that is hard to see any
other way.

Also says plainly what each --type needs. Only `policies` works without
the proxy being reachable; the rest relay Envoy admin output and need the
zone connected and the proxy up, with the failure at either hop reported
as it arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Universal is where the responsibility split actually matters, so the set
leads with that rather than with installation:

- /mesh/universal/ — what Universal is for, a concern-by-concern table of
  Kubernetes against Universal, and the changes that only bite here.
- /mesh/universal-control-plane/ — modes, stores, joining a zone to a
  global, and what the control plane generates.
- /mesh/universal-data-plane/ — the Dataplane resource, labels, inbounds,
  outbounds, transparent proxying, readiness, zone proxies and tokens.

Reading the code corrected the premise I started from. The control plane
does *not* leave MeshServices to the user on Universal: a zone control
plane on a non-Kubernetes store generates one per distinct
`kuma.io/workload` label value, with a port per inbound, reconciled every
2s by default and removed an hour after the last proxy goes. Same for
Workload resources. The generator is skipped for Global mode, for a
Kubernetes store, and when the interval is set to 0 -- at which point it
logs `MeshServices must be managed manually`. So the rule the pages give
is: set `kuma.io/workload` and the MeshService follows; omit it and
nothing is generated.

Two findings reshape how much Universal matters in 3.0:

- A Global control plane can no longer run on Kubernetes at all. `mode:
  global` is rejected with `environment: kubernetes` and again with
  `store.type: kubernetes`. Every multi-zone deployment now has a
  Universal control plane in it, whatever the zones run.
- Standalone zone proxies are gone. Cross-zone traffic runs on ordinary
  Dataplanes with `networking.listeners` of type ZoneIngress/ZoneEgress,
  authenticating with dataplane tokens rather than zone tokens.

Three v3 changes are silent and land only on Universal, so each is a
warning: inbound `tags` removed and dropped on read; outbound `tags`
replaced by a required `backendRef`, where a stored resource keeps being
served but generates no listener; and `networking.inbound[].protocol` no
longer falling back to the `kuma.io/protocol` tag, which turns a port TCP
and strips its L7 policies.

The Dataplane YAML could not be applied to the live control plane --
`Dataplane is read only on this control plane`, since it is a Kubernetes
zone and Pods generate them, which is itself the point these pages make.
It is instead checked field by field against the proto and against
test/framework/dataplane_template.go, the canonical Universal fixture,
whose own comment confirms the framing: identity is "sourced solely from
Dataplane labels, not inbound tags".

Six superseded v2 pages now point at these as their canonical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
New pages for v3 alongside the v2 series, not replacing it: the v2 pages
stay at /mesh/v2/get-started/universal/ untouched, and these live at
/mesh/get-started/universal/.

Given its own series id, `mesh-get-started-universal-3`, because the
series generator groups purely by `series.id` with no version awareness.
Sharing the v2 id would have put ten pages with duplicate positions 1-5
into one series, and next/prev use `.find` on position, so a v3 page would
have linked to a v2 page.

That is not hypothetical. `mesh-scoped-zone-proxy` already shares an id
across both versions, and the built page proves the consequence: the
latest /mesh/zone-proxies/ page's "Next" link points at
/mesh/v2/zone-proxy-policies/, sending a v3 reader into v2 docs. Left
alone here as pre-existing, but it wants the same fix.

Page 1 covers the install and the two files the rest of the series reuses,
and uses the Dataplane template to introduce the three things Universal
actually asks of you: the kuma.io/workload label that drives MeshService
generation, ordinary labels now that inbounds carry no tags, and the
inbound protocol that silently costs you every L7 policy when omitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five pages at /mesh/get-started/universal/, additive to the v2 series
which stays untouched at /mesh/v2/get-started/universal/.

Page 5 is a rewrite rather than an adaptation, because most of what the
v2 version teaches no longer exists: `Mesh.mtls` and `meshServices.mode`
are removed from the Mesh schema, MeshTrafficPermission's `from` array,
`action` field and MeshSubset targeting are removed, and the entire
built-in gateway is gone -- MeshGateway, MeshGatewayRoute,
MeshGatewayInstance and MeshGatewayConfig, their CRDs, and MeshGateway as
a targetRef kind. So it teaches MeshIdentity, spiffeID allow rules, and
names the delegated gateway as the replacement rather than walking
through a gateway that cannot be built.

It also states the 2.x deny-all-then-allow idiom is now wrong: every deny
is evaluated before every allow, so a mesh-wide deny can never be
re-opened.

The series carries its own id, mesh-get-started-universal-3, and the v2
entry in series.yml is repointed at the v2 install page -- it was aimed at
the v3 URL while all five of its pages are v2, which sent readers across
versions. Verified: zero links into /mesh/v2/ from any of the five pages,
and the series navigation resolves among themselves.

One invented command caught before it shipped: page 2 had `kongctl config
set`, which does not exist. kongctl takes configuration from a
profile-keyed YAML file at $XDG_CONFIG_HOME/kongctl/config.yaml, so the
page now shows that file and uses --control-plane-url explicitly
throughout.

The container steps are not executed end to end -- no Universal control
plane was available -- so they are written from the flags and config the
source defines, and every kongctl command shown exists in the binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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
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.

2 participants