Skip to content
---
title: "Gateway API Annotation Placement Clarity"
version: v1alpha1
authors: "@lexfrei"
creation-date: 2025-10-23
status: provisional
---

Gateway API Annotation Placement Clarity

Table of Contents

Summary

The annotations documentation
indicates that Gateway API sources support various annotations, but it does not clearly specify which Kubernetes
resource (Gateway vs HTTPRoute/GRPCRoute/TLSRoute/etc.) these annotations should be placed on. This ambiguity leads
to user confusion and misconfigurations.

This proposal aims to:

  1. Short-term: Improve documentation to explicitly clarify annotation placement
  2. Long-term: Consider implementing annotation inheritance from Gateway to Routes

Motivation

Users frequently misconfigure annotations when using Gateway API sources because the current documentation uses “Gateway” as the source name in the annotation support table, which is ambiguousit refers to gateway-api sources generically, not the Gateway resource specifically.

Current Implementation Behavior

Based on the source code
(source/gateway.go):

Gateway resource annotations:

  • external-dns.alpha.kubernetes.io/target - read from Gateway
    (line ~380)

Route resource annotations (HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute):

  • external-dns.alpha.kubernetes.io/hostname - read from Route
  • external-dns.alpha.kubernetes.io/ttl - read from Route
  • external-dns.alpha.kubernetes.io/controller - read from Route
  • Provider-specific annotations (e.g., cloudflare-proxied, aws/*, scw/*, etc.) - read from Route
    (line ~242)

This separation aligns with Gateway API architecture:

  • Gateway = infrastructure layer (IP addresses, listeners, load balancers)
  • Routes = application layer (DNS records, routing rules, hostnames)

However, users expect provider-specific annotations to work on Gateway (similar to how target works), leading to silent failures.

Goals

  • Clarify annotation placement in documentation to prevent user confusion
  • Provide practical examples for common providers (Cloudflare, AWS, Scaleway)
  • Define a clear, documented contract for where each annotation type should be placed
  • Reduce support burden from repeated misconfigurations

Non-Goals

  • This proposal does not address the broader annotation standardization effort discussed in PR #5080
  • Redesigning the Gateway API source implementation
  • Changing behavior for non-Gateway sources (Ingress, Service, etc.)
  • Making breaking changes to existing Gateway API functionality

Proposal

User Stories

Story 1: Platform Engineer with Cloudflare (#5901)

As a platform engineer, I set up a Gateway with the external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
annotation, expecting all DNS records for Routes using this Gateway to be proxied through Cloudflare. However, the
annotation is silently ignored, and records are created without proxy, leading to unexpected traffic routing and
security issues.

Root cause: Had to dive into source code to discover that provider-specific annotations are only read from
Route resources, not Gateway resources.

Current workaround: Must copy the cloudflare-proxied annotation to every HTTPRoute manually.

Story 2: User Attempting Route-Specific Targets (#4056)

As a user, I want to specify different target DNS records for specific hosts while sharing a common Gateway. I
added external-dns.alpha.kubernetes.io/target annotation on HTTPRoute to override the Gateway’s target for one
specific host, but it doesn’t work - the annotation is ignored on HTTPRoute.

Root cause: The target annotation must be on the Gateway resource, not on Route resources. There’s no way to
override targets on a per-Route basis.

Outcome: User had to find alternative workarounds to exclude specific hosts or create separate Gateway resources.

Current Behavior

Annotation Placement Matrix

Annotation Type Gateway Resource Route Resources (HTTPRoute, GRPCRoute, etc.)
target Read from Gateway L Ignored
hostname L Not used Read from Route
ttl L Not used Read from Route
controller L Not used Read from Route
Provider-specific (cloudflare-proxied, aws/*, scw/*) L Not used Read from Route

Code References

// source/gateway.go line ~380
// Target annotation is read from Gateway
override := annotations.TargetsFromTargetAnnotation(gw.gateway.Annotations)

// source/gateway.go line ~242
// Provider-specific annotations are read from Route
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)

Where annots is derived from the Route’s metadata (meta.Annotations), not the Gateway.

Proposed Solutions

Solution 1: Documentation Improvements (Short-term - Quick Win)

Implementation Status: Documentation improvements proposed in PR #5918.

Note: If Solution 2 (Annotation Merging) is implemented, the documentation from PR #5918 will require updates
to reflect the new inheritance behavior.

Changes to docs/annotations/annotations.md:

Expand footnote [^4] or add a new section “Gateway API Annotation Placement” with a detailed table:

### Gateway API Annotation Placement

When using Gateway API sources (gateway-httproute, gateway-grpcroute, etc.), annotations must be placed on specific resources:

| Annotation | Placement | Example Resource |
|------------|-----------|------------------|
| `target` | Gateway | `kind: Gateway` |
| `hostname` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `ttl` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `controller` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `cloudflare-proxied` | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `aws-*` (all AWS annotations) | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |
| `scw-*` (all Scaleway annotations) | Route | `kind: HTTPRoute`, `kind: GRPCRoute`, etc. |

**Rationale**: The Gateway resource defines infrastructure (IP addresses, listeners), while Routes define application-level DNS records. Therefore, DNS record properties (TTL, provider settings) are configured on Routes.

Changes to docs/sources/gateway-api.md:

Add a new section after “Hostnames”:

## Annotations

### Annotation Placement

ExternalDNS reads different annotations from different Gateway API resources:

- **Gateway annotations**: Only `external-dns.alpha.kubernetes.io/target` is read from Gateway resources
- **Route annotations**: All other annotations (hostname, ttl, provider-specific) are read from Route resources

#### Example: Cloudflare Proxied Records

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-gateway
  namespace: default
  annotations:
    #  Correct: target annotation on Gateway
    external-dns.alpha.kubernetes.io/target: "203.0.113.1"
spec:
  gatewayClassName: cilium
  listeners:
    - name: https
      hostname: "*.example.com"
      protocol: HTTPS
      port: 443
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-route
  annotations:
    #  Correct: provider-specific annotations on HTTPRoute
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
    external-dns.alpha.kubernetes.io/ttl: "300"
spec:
  parentRefs:
    - name: my-gateway
      namespace: default
  hostnames:
    - api.example.com
  rules:
    - backendRefs:
        - name: api-service
          port: 8080

Example: AWS Route53 with Routing Policies

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: aws-gateway
  annotations:
    #  Correct: target annotation on Gateway
    external-dns.alpha.kubernetes.io/target: "alb-123.us-east-1.elb.amazonaws.com"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: weighted-route
  annotations:
    #  Correct: AWS-specific annotations on HTTPRoute
    external-dns.alpha.kubernetes.io/aws-weight: "100"
    external-dns.alpha.kubernetes.io/set-identifier: "backend-v1"
spec:
  parentRefs:
    - name: aws-gateway
  hostnames:
    - app.example.com

Common Mistakes

Incorrect: Placing provider-specific annotations on Gateway

kind: Gateway
metadata:
  annotations:
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"  # ❌ Ignored

Incorrect: Placing target annotation on HTTPRoute

kind: HTTPRoute
metadata:
  annotations:
    external-dns.alpha.kubernetes.io/target: "203.0.113.1"  # ❌ Ignored

Implementation effort: Low
Maintenance burden: Minimal (documentation only)
User benefit: Immediate clarity, reduced misconfiguration

Solution 2: Annotation Inheritance and Merging (Long-term - Feature Enhancement)

Reference Implementation: PR #5998

Implement annotation merging logic where:

  1. Gateway annotations serve as defaults for all Routes attached to that Gateway
  2. Route annotations override Gateway annotations for specific Routes
  3. All annotations are inheritable, including target — enabling per-Route target overrides

Proposed implementation (pseudocode):

// source/gateway.go - proposed changes
func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
    // ... existing code ...

    for _, route := range routes {
        // Merge Gateway and Route annotations
        // Route annotations take precedence over Gateway annotations
        gwAnnots := gw.gateway.Annotations
        rtAnnots := route.meta.Annotations
        mergedAnnots := mergeAnnotations(gwAnnots, rtAnnots)

        // Use merged annotations for all annotation processing
        providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(mergedAnnots)
        ttl := annotations.TTLFromAnnotations(mergedAnnots, resource)

        // ... rest of endpoint creation ...
    }
}

// Helper function
func mergeAnnotations(gateway, route map[string]string) map[string]string {
    merged := make(map[string]string, len(gateway)+len(route))

    // Copy Gateway annotations (defaults)
    for k, v := range gateway {
        merged[k] = v
    }

    // Route annotations override Gateway defaults
    for k, v := range route {
        merged[k] = v
    }

    return merged
}

Example use case enabled by this approach:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: intranet-gateway
  annotations:
    # Default target for internal services
    external-dns.alpha.kubernetes.io/target: "172.16.6.6"
    # Set default for all Routes using this Gateway
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
    external-dns.alpha.kubernetes.io/ttl: "300"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: internal-api
  # Inherits: target=172.16.6.6, cloudflare-proxied=true, ttl=300 from Gateway
spec:
  parentRefs:
    - name: intranet-gateway
  hostnames:
    - api.internal.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: public-api
  annotations:
    # Override: expose this route to the public internet
    external-dns.alpha.kubernetes.io/target: "203.0.113.1"
    # Inherits: cloudflare-proxied=true, ttl=300 from Gateway
spec:
  parentRefs:
    - name: intranet-gateway
  hostnames:
    - api.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: static-assets
  annotations:
    # Override: disable proxying for static content
    external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
    # Inherits: target=172.16.6.6, ttl=300 from Gateway
spec:
  parentRefs:
    - name: intranet-gateway
  hostnames:
    - static.internal.example.com

This example demonstrates a common use case: an intranet Gateway where most services are internal
(172.16.6.6), but specific Routes can be exposed publicly (203.0.113.1) by overriding the
target annotation.

Benefits:

  • Reduces configuration duplication
  • Enables centralized defaults at Gateway level
  • Maintains flexibility with Route-level overrides
  • Better matches user mental model (infrastructure defaults + application overrides)
  • Solves User Story 2: Enables per-Route target overrides without creating separate Gateways

Risks:

  • Backward compatibility concerns (may change behavior for existing users)
  • Increased code complexity
  • Potential for confusion about precedence rules
  • Need for comprehensive testing across all Gateway API Route types

Mitigation strategies:

  • Feature flag to opt-in to new behavior initially
  • Clear documentation of precedence rules
  • Extensive test coverage
  • Migration guide for users

Implementation effort: Medium
Maintenance burden: Medium (code + tests + docs)
User benefit: Significant reduction in configuration overhead

Drawbacks

Documentation-Only Solution

  • Does not address the underlying UX issue (annotation duplication)
  • Requires users to manually propagate settings across Routes
  • Still allows silent failures if users misplace annotations

Annotation Merging Solution

  • Adds complexity to the codebase
  • Requires careful consideration of precedence rules
  • May introduce unexpected behavior changes for existing users
  • Needs comprehensive testing for edge cases (multiple Gateways, cross-namespace, etc.)
  • Potential performance impact from annotation merging on every reconciliation

Alternatives

Alternative 1: Do Nothing (Status Quo)

Description: Keep current behavior and documentation as-is.

Pros:

  • No implementation effort required
  • No risk of introducing new bugs
  • No breaking changes

Cons:

  • Users continue to experience confusion and misconfigurations
  • Increased support burden on maintainers and community
  • Poor user experience compared to other sources (Ingress supports annotations more intuitively)

Recommendation: L Not recommended - problem is well-documented and affects user productivity

Alternative 2: Move All Annotations to Gateway Only

Description: Refactor source code to read all annotations from Gateway, not Routes.

Pros:

  • Simplified mental model (one place for all annotations)
  • Centralized configuration

Cons:

  • Breaks Gateway API architecture - Routes define application-layer DNS records, so DNS properties belong on Routes
  • Cannot have different settings per Route (e.g., different TTLs for api.example.com vs static.example.com)
  • Loses flexibility that Route-level annotations provide
  • Requires breaking change to existing implementations

Recommendation: L Not recommended - violates Gateway API design principles

Alternative 3: Support Annotations on Both with Strict Validation

Description: Allow annotations on both Gateway and Route, but error/warn if duplicates exist without clear precedence.

Pros:

  • Provides flexibility
  • Catches configuration errors explicitly

Cons:

  • Confusing for users (two valid places to configure)
  • Requires complex validation logic
  • Still doesn’t solve the “defaults + overrides” use case
  • More complex to document and support

Recommendation: � Possible but adds complexity without solving core UX issue

Alternative 4: Create Dedicated GatewayDNSConfig CRD

Description: Introduce a new CRD that defines DNS configuration separately from Gateway and Route resources.

Example:

apiVersion: externaldns.k8s.io/v1alpha1
kind: GatewayDNSConfig
metadata:
  name: cloudflare-defaults
spec:
  gatewayRef:
    name: my-gateway
  defaults:
    ttl: 300
    providerSpecific:
      - name: cloudflare-proxied
        value: "true"
---
apiVersion: externaldns.k8s.io/v1alpha1
kind: RouteDNSConfig
metadata:
  name: api-route-dns
spec:
  routeRef:
    kind: HTTPRoute
    name: api-route
  overrides:
    ttl: 60  # Override Gateway default

Pros:

  • Clean separation of concerns
  • Clear precedence model
  • No annotations needed (type-safe CRDs)
  • Aligns with Kubernetes resource composition patterns

Cons:

  • Significant implementation effort (new CRDs, controllers, validation, etc.)
  • Adds complexity with additional resources to manage
  • Requires migration from annotation-based approach
  • Diverges from how other sources work (Ingress, Service use annotations)
  • May conflict with future annotation standardization efforts

Recommendation: � Potentially valuable long-term, but scope is too large for this specific issue

Alternative 5: Wait for Annotation Standardization (PR #5080)

Description: Defer this work until the broader annotation standardization effort is resolved.

Pros:

  • Avoids potentially redundant work
  • May be addressed as part of larger effort

Cons:

  • PR #5080 is not yet ready for review and timeline is uncertain
  • Users continue to experience issues in the meantime
  • Documentation improvements are still valuable regardless of standardization outcome

Recommendation: � Partial - implement documentation improvements now (Solution 1), reconsider
annotation merging after standardization is resolved

Recommendation

Phased approach:

  1. Immediate (v0.15.0 or next minor): Implement Solution 1 (Documentation Improvements)

    • Low risk, high user value
    • Can be merged quickly
    • Addresses immediate pain points
  2. Near-term: Review and merge Solution 2 (Annotation Merging)

    • Reference implementation available: PR #5998
    • Includes comprehensive test coverage
    • Backward compatible (no breaking changes for existing configurations)
    • Solves User Story 2 (per-Route target overrides)
  3. Future (post-PR #5080 resolution): Re-evaluate if additional changes are needed

    • Assess compatibility with annotation standardization outcomes
    • Gather user feedback on the annotation inheritance behavior

This approach provides immediate relief while keeping options open for more comprehensive solutions in the future.