Files
helm-gitea/docs/gateway-api.md
T
Todd MarimonandMarkus Pesch 7747a001f7 feat: add Gateway API support (#1073)
Add full Gateway API support for exposing Gitea via HTTPRoute, TCPRoute, BackendTLSPolicy, and ClientSettingsPolicy resources.

New templates:
- `httpRoute.yaml` — renders an HTTPRoute with configurable
  parentRefs, hostnames, and rules (defaults to PathPrefix `/`)
- `tcpRoute.yaml` — renders a TCPRoute for SSH traffic
- `backendTLSPolicy.yaml` — renders a BackendTLSPolicy for
  encrypted backend connections with required validation config
- `clientSettingsPolicy.yaml` — renders an NGINX Gateway Fabric
  ClientSettingsPolicy to raise the request body size limit

Infrastructure:
- `gatewayAPI.enabled` global toggle gates all resources
- Resources grouped under `gatewayAPI.core.*` and `gatewayAPI.nginx.*`
- Helper templates extracted into dedicated `_*.tpl` files
- Service name helpers (`gitea.service.http.name`, `gitea.service.ssh.name`)
  extracted into `_services.tpl`; service templates renamed to camelCase
- `ROOT_URL`, `DOMAIN`, and `SSH_DOMAIN` auto-resolve from
  `httpRoute.hostnames[0]`; `httpRoute.tls` switches to `https`

Documentation:
- New `docs/gateway-api.md` with topology examples, BackendTLSPolicy
  setup, sectionName guidance, SSH considerations, and NGINX body
  size limit configuration
- `.github/copilot-instructions.md` with project conventions
- README parameter table auto-generated via `make readme`

Tests:
- Helm unit tests for all four new resource templates
- Config tests for hostname/TLS resolution from Gateway API values

Co-authored-by: Todd Marimon <toddmarimon@gmail.com>
2026-07-19 16:25:28 +00:00

10 KiB

Gateway API

This chart can expose Gitea through Kubernetes Gateway API resources alongside (or instead of) the existing Ingress and OpenShift Route support. The following resources are rendered:

  • HTTPRoute — required for HTTP traffic
  • TCPRoute — optional, typically for SSH (port 22)
  • BackendTLSPolicy — optional, for encrypted backend traffic
  • ClientSettingsPolicy — optional, NGINX Gateway Fabric only, to raise the client request body size limit

All resources are disabled by default. Enabling them requires Gateway API CRDs (and an implementation that supports them) to already be installed in the cluster.

The chart does not render a Gateway resource — provisioning and managing the Gateway is the responsibility of the cluster / platform administrator.

Prerequisites

Resource API version Status (as of writing)
HTTPRoute gateway.networking.k8s.io/v1 GA
TCPRoute gateway.networking.k8s.io/v1alpha2 Experimental
BackendTLSPolicy gateway.networking.k8s.io/v1 GA (v1.2+)
ClientSettingsPolicy gateway.nginx.org/v1alpha1 NGINX Gateway Fabric

Common topology

Most users should attach to a pre-existing, shared Gateway managed by the cluster administrator:

gatewayAPI:
  core:
    httpRoute:
      enabled: true
      tls: true            # the shared Gateway terminates TLS
      hostnames:
        - git.example.com
      parentRefs:
        - group: gateway.networking.k8s.io
          kind: Gateway
          name: shared-gateway
          namespace: gateway-system
          sectionName: https-gitea   # pin to a specific listener (see below)
    tcpRoute:
      enabled: true
      parentRefs:
        - group: gateway.networking.k8s.io
          kind: Gateway
          name: shared-gateway
          namespace: gateway-system
          sectionName: ssh

With this configuration:

  • ROOT_URL, DOMAIN, and SSH_DOMAIN resolve to the first HTTPRoute hostname.
  • Setting gatewayAPI.core.httpRoute.tls: true switches ROOT_URL to https://.
  • The default HTTPRoute rule forwards / to the Gitea HTTP Service. The default TCPRoute rule forwards to the SSH Service.
  • Custom rules and hostnames are rendered through tpl, so Helm template expressions work inside them.

Why sectionName matters

Omitting sectionName attaches the route to every matching listener on the Gateway. On implementations that use per-host HTTPS listeners (Envoy Gateway, Cilium Gateway), that means Gitea's HTTPRoute will try to bind to every HTTPS listener — usually not what you want. Always pin to a named listener (e.g. https-gitea, ssh) when the Gateway has more than one. The corresponding listener on the Gateway side typically looks like:

listeners:
  - name: https-gitea
    port: 443
    protocol: HTTPS
    hostname: git.example.com
    tls:
      certificateRefs:
        - name: git-example-com-tls
    allowedRoutes:
      kinds:
        - kind: HTTPRoute
      namespaces:
        from: Selector
        selector:
          matchLabels:
            kubernetes.io/metadata.name: gitea
  - name: ssh
    port: 22
    protocol: TCP
    allowedRoutes:
      kinds:
        - kind: TCPRoute
      namespaces:
        from: Selector
        selector:
          matchLabels:
            kubernetes.io/metadata.name: gitea

Sharing a hostname between HTTP and SSH

HTTP (443) and SSH (22) are different ports, so a single hostname like git.example.com can serve both — clients disambiguate by port. This is the recommended pattern: one DNS record, ssh git@git.example.com and https://git.example.com both work, and SSH_DOMAIN / DOMAIN resolve to the same value with no extra configuration.

If you want SSH on a different hostname (e.g. gitea-ssh.example.com), set it explicitly — the chart cannot infer it from TCPRoute config because TCPRoutes don't carry hostnames:

gitea:
  config:
    server:
      SSH_DOMAIN: gitea-ssh.example.com

BackendTLSPolicy

Use this when the Gitea HTTP backend is terminating TLS itself (for example, when running Gitea with PROTOCOL=https, or when fronting another HTTPS service from the same chart) and the Gateway needs to verify the backend certificate before forwarding the request.

Configuring Gitea to serve HTTPS directly

Gitea serves HTTPS via three [server] app.ini options (cheat sheet). Mount the cert/key with extraVolumes + extraContainerVolumeMounts and point Gitea at them with absolute paths:

gitea:
  config:
    server:
      PROTOCOL: https
      CERT_FILE: /etc/gitea-tls/tls.crt
      KEY_FILE: /etc/gitea-tls/tls.key

extraVolumes:
  - name: gitea-tls
    secret:
      secretName: gitea-backend-tls   # cert-manager-issued Secret, etc.
extraContainerVolumeMounts:
  - name: gitea-tls
    mountPath: /etc/gitea-tls
    readOnly: true
  • Relative CERT_FILE/KEY_FILE values resolve against Gitea's CustomPath (/data/gitea in the official image); absolute paths are clearer.
  • Both options are ignored when gitea.config.server.ENABLE_ACME is true.
  • For chained certs, the server cert comes first, intermediates after.
  • The Service still forwards raw TCP — no service.http.* changes needed. The pod's container port (3000 by default) is now speaking HTTPS instead of HTTP.

BackendTLSPolicy example

Verify the backend with a CA bundle stored in a ConfigMap:

gatewayAPI:
  core:
    backendTLSPolicy:
      enabled: true
      validation:
        hostname: gitea.svc.cluster.local
        caCertificateRefs:
          - name: gitea-backend-ca
            group: ""
            kind: ConfigMap

This renders a single BackendTLSPolicy whose targetRefs defaults to the chart's HTTP Service (<fullname>-http), and whose validation is passed through verbatim. validation is required by the API; the template fails fast if omitted.

System CA trust and explicit targetRefs

To trust the system CA store (Gateway API v1.1+) or target a different Service, use wellKnownCACertificates and targetRefs:

gatewayAPI:
  core:
    backendTLSPolicy:
      enabled: true
      targetRefs:
        - group: ""
          kind: Service
          name: gitea-sidecar
      validation:
        hostname: sidecar.gitea.svc.cluster.local
        wellKnownCACertificates: System

Notes:

  • targetRefs[].kind is almost always Service; group: "" is the core API group.
  • wellKnownCACertificates: System requires Gateway API v1.1 and an implementation that supports it (otherwise stick with caCertificateRefs).
  • The corresponding HTTPRoute must reference the backend by the same Service (and, if used, sectionName/port) — BackendTLSPolicy attaches to the Service-side reference, not to the route.

Raising the request body size limit (NGINX Gateway Fabric)

NGINX defaults client_max_body_size to 1m. Requests exceeding it are rejected with 413 Request Entity Too Large. This blocks uploading larger artifacts to Gitea's package/container registry (container images, DEB/RPM packages, etc.). With the NGINX Ingress controller you raised this via the nginx.ingress.kubernetes.io/proxy-body-size annotation — that annotation does not apply to Gateway API. NGINX Gateway Fabric instead reads the limit from a ClientSettingsPolicy (spec.body.maxSize).

This is specific to NGINX Gateway Fabric. Other implementations (Envoy Gateway, Cilium, Istio, …) do not impose a default request body size limit, so large uploads work without any extra configuration — leave gatewayAPI.nginx.clientSettingsPolicies disabled.

gatewayAPI:
  enabled: true
  nginx:
    clientSettingsPolicies:
      enabled: true
      body:
        maxSize: 100m   # bytes, or with a k / m / g suffix; 0 disables the limit

This renders a single ClientSettingsPolicy whose targetRef defaults to the chart's HTTPRoute (<fullname>), so the limit applies to all traffic routed to Gitea. body is required when enabled; the template fails fast if omitted. spec.body is passed through verbatim, so other fields (e.g. timeout) are supported too.

To attach the policy elsewhere — for example the whole Gateway so the limit is inherited by every route — override targetRef:

gatewayAPI:
  nginx:
    clientSettingsPolicies:
      enabled: true
      targetRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: shared-gateway
      body:
        maxSize: 100m

Notes:

  • ClientSettingsPolicy is an inherited policy: attaching it to a Gateway cascades to its routes, while attaching it to an HTTPRoute scopes it to that route only.
  • The policy must live in the same namespace as its targetRef.
  • Gitea also enforces its own upload limits independently (gitea.config [repository.upload] and [packages] sections) — raising the proxy limit alone is not always sufficient.

Interaction with ingress and route

The three exposure mechanisms are independent and can coexist, but ROOT_URL / DOMAIN / SSH_DOMAIN resolution uses the first defined source in this order:

  1. route.host (when route.enabled)
  2. httpRoute.hostnames[0] (when gatewayAPI.core.httpRoute.enabled)
  3. First ingress.hosts[0].host
  4. The in-cluster Service DNS name

Likewise, ROOT_URL becomes https:// if any of these terminate TLS: route.tls.termination, ingress.tls, or gatewayAPI.core.httpRoute.tls.

SSH considerations

  • TCPRoute is still experimental. Many production-grade implementations support it (Envoy Gateway, Istio, Kgateway, NGINX Gateway Fabric), but you should verify before relying on it.
  • If your Gateway implementation does not support TCPRoute, keep using service.ssh.type: LoadBalancer (or NodePort) and only enable httpRoute for HTTP traffic.
  • The default TCPRoute rule points at the Gitea SSH Service on service.ssh.port (typically 22), which itself proxies to gitea.config.server.SSH_LISTEN_PORT inside the pod.