Smart annotations for Gateway routes

Last published : Nov 02, 2026
Smart annotations let you configure NetScaler® entities directly from your Gateway and route objects, using annotations of the form gateway.citrix.com/<nsresource>. A single, registry-driven engine handles every supported resource — LB monitor, load balancing virtual server, service group, and content switching virtual server — so you tune the NetScaler configuration alongside your Kubernetes manifests, without any CLI or GUI configuration on the NetScaler instance.
This is a cross-cutting feature. It applies to every route kind that the Gateway Controller programs (Gateway, HTTPRoute, TCPRoute, UDPRoute, AIGatewayRoute, and mcproute), not to MCP routes alone.

Annotation form

The annotation key is gateway.citrix.com/<nsresource>. Place it on any Gateway or route object the Gateway Controller programs. The target NetScaler entity depends on whether the object is a Gateway (front end) or a route (back end).
Object Target NetScaler entity
Gateway The Gateway's front-end content switching (CS) virtual server
HTTPRoute (Gateway API) The route's back-end service group / LB virtual server
AIGatewayRoute The route's back-end service group / LB virtual server
TCPRoute (Gateway API) The route's back-end service group / LB virtual server
UDPRoute (Gateway API) The route's back-end service group / LB virtual server
mcproute The route's back-end service group / LB virtual server
Note:
There is no TLSRoute carrier — to configure the HTTPS front end, place gateway.citrix.com/csvserver on the Gateway object.

Modes

Each supported resource is applied in one of two modes.
  • create_bind — The controller creates a standalone NetScaler object and binds it to the target entity. For example, an lbmonitor is created and bound to the route's back-end service group. When the value is a bare string, that string names a preconfigured object that already exists on the NetScaler, and the controller only binds it (it never creates or deletes it).
  • set — The controller sets NITRO attributes on an entity that it already owns. This is how you tune the controller-created lbvserver, servicegroup, or csvserver.

Value model

An annotation value is parsed as YAML or JSON, and can take three shapes:
  • String — Names a preconfigured NetScaler object. This is valid for create_bind resources only (bind-only; the controller never creates or removes the named object).
  • Mapping — A flat set of NITRO attributes for the resource, plus the reserved schema keys below. Any attribute the NetScaler resource supports is accepted.
  • List — A list of strings and/or mappings, applied in order.
The reserved keys in a mapping are consumed by the annotation engine and are never sent to the NetScaler as attributes:
Reserved key Meaning
name For a preconfigured item (preconfigured: true), the name of the existing NetScaler object to bind. For a controller-created object, name is ignored — the controller derives the name from the route.
preconfigured true marks the item as bind-only against an existing object. Requires name. Valid for create_bind resources only.
backendRefs Scopes the entry to specific back ends. Each entry is a {name, namespace, port} mapping matching a backendRef of the route, so the setting applies only to those back ends' service groups or LB virtual servers. When omitted, the entry applies to all of the route's back ends. Not applied to lbmonitor entries: a monitor is bound to the service groups of all the route's back ends. On an mcproute, servicegroup and sslservicegroup entries also apply to all of the route's back ends; only lbvserver entries honor backendRefs there.
Note:
The mapping is a flat set of attributes — it is not keyed by service name. This differs from the NetScaler Ingress Controller annotations (for example, ingress.citrix.com/servicegroup: '{"appname": {...}}'). To target one back end rather than all of them, use the backendRefs reserved key. This does not apply to lbmonitor entries, or to servicegroup and sslservicegroup entries on an mcproute.

Supported resources

Resource Mode Target entity Description
lbmonitor create_bind service group Creates an LB monitor and binds it to the route's back-end service group. A bare string binds a preconfigured monitor.
lbvserver set LB virtual server Sets attributes on the route's back-end LB virtual server (for example, session persistence).
servicegroup set service group Sets attributes on the route's back-end service group (for example, client IP insertion).
sslservicegroup set service group Sets SSL parameters on the back-end service group. Intended for SSL service groups; a non-SSL back end tolerates the setting rather than failing the route.
csvserver set CS virtual server Sets attributes on the Gateway (or route) front-end CS virtual server.

Configure CS, LB, and service group settings

CS virtual server on a Gateway

Place gateway.citrix.com/csvserver on the Gateway object to set attributes on its front-end CS virtual server.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mcp-gw
  namespace: default
  annotations:
    gateway.citrix.com/csvserver: |
      l2conn: "ON"
spec:
  gatewayClassName: mcpgwclass
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
EOF

LB virtual server on a route

Place gateway.citrix.com/lbvserver on a route to set attributes on its back-end LB virtual server. The following example enables rule-based session persistence keyed on an MCP session header, so all requests in a session pin to the same back end.
kubectl apply -f - <<EOF
apiVersion: citrix.com/v1
kind: mcproute
metadata:
  name: mcp-github
  namespace: default
  annotations:
    gateway.citrix.com/lbvserver: |
      persistenceType: RULE
      rule: 'http.req.header("Mcp-Session-Id")'
      resRule: 'http.res.header("Mcp-Session-Id")'
spec:
  parentRefs:
    - name: mcp-gw
      namespace: default
  rules:
    - matches:
        - headers:
            - name: x-netscaler-target-mcp-server
              type: MCPTargetServer
              value: github.mcp.example
      backendRefs:
        - name: mcp-github
          port: 443
EOF

Service group on a route

Place gateway.citrix.com/servicegroup on a route to set attributes on its back-end service group. The following example turns on client IP insertion and sets the header the client IP is carried in.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mcp-github
  namespace: default
  annotations:
    gateway.citrix.com/servicegroup: |
      cip: ENABLED
      cipHeader: X-Forwarded-For
spec:
  parentRefs:
    - name: mcp-gw
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: mcp-github
          port: 80
EOF
For an SSL back end, use gateway.citrix.com/sslservicegroup to set SSL parameters on the back-end service group. The following annotation turns off SSL session reuse, for example for MCP streaming:
metadata:
  annotations:
    gateway.citrix.com/sslservicegroup: |
      sessReuse: DISABLED

Configure a load balancing monitor

Attach a load balancing monitor to a route's back-end service group with the gateway.citrix.com/lbmonitor annotation. The Gateway API routes (HTTPRoute, TCPRoute, UDPRoute), AIGatewayRoute, and mcproute all use it. The annotation value is one of two kinds:
  • A monitor that the controller creates. Give a mapping of NITRO lbmonitor attributes, such as type, httpRequest, interval, and respTimeout. The controller creates the monitor and binds it to the route's back-end service group. The controller derives the monitor name from the service group, so any name that you supply is ignored.
  • A monitor that you already created on NetScaler. Give only the name of the monitor, as a plain string. The controller binds this monitor to the route's back-end service group. It never creates, changes, or deletes the monitor.
Example 1: The controller creates the monitor. The following example creates an HTTP monitor that sends HEAD /healthz to the back-end service every 10 seconds.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-route
  namespace: default
  annotations:
    gateway.citrix.com/lbmonitor: |
      type: HTTP
      httpRequest: "HEAD /healthz"
      interval: 10
      respTimeout: 3
spec:
  parentRefs:
    - name: mcp-gw
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: web-svc
          port: 80
EOF
Example 2: You bind a monitor that already exists on NetScaler. First, create the monitor on NetScaler. For example:
add lb monitor my-http-monitor HTTP -respCode 200 -httpRequest "HEAD /healthz"
Then, set the annotation to the name of the monitor. my-http-monitor is only an example. Replace it with the name of your own monitor.
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-route
  namespace: default
  annotations:
    gateway.citrix.com/lbmonitor: my-http-monitor
spec:
  parentRefs:
    - name: mcp-gw
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: web-svc
          port: 80
EOF
The monitor must exist on NetScaler before you apply the route. The controller does not create it, and the binding fails if the monitor is missing.
Note:
You can also write an existing monitor as a mapping, with name: my-http-monitor and preconfigured: true. Without preconfigured: true, the controller ignores name and does not bind your monitor.
For a TCP back end, use type: TCP in the mapping. To bind more than one monitor, give a YAML list. The list can contain both kinds of monitor.
When you remove a monitor from the annotation, remove the whole annotation, or delete the route, the controller deletes a monitor that it created (with its binding), but for a monitor that you created on NetScaler, it removes only the binding and leaves the monitor in place.

Modify and unset behavior

A NetScaler set changes only the attributes it is given and never clears the rest, so the engine tracks changes across updates:
  • Attribute removed on an update (set and create_bind) — An attribute that was present on the previous version of the object but is absent now is explicitly unset back to its NetScaler default. A dropped respTimeout or sessReuse, for example, does not linger as configuration drift.
  • Whole annotation removed — Removing the annotation entirely tears down the objects the controller created for it (for example, a controller-created monitor and its binding), in reverse order. Preconfigured objects referenced by name are left intact — only their binding is removed.
Note:
A controller-created object is named from the route, and a per-item name is derived per target. If an entry names an object the route does not own, points an entity at another controller-owned object, or renames the controller-created object, the engine drops that attribute and logs a warning; the rest of the entry still applies.

Reference