From 47aca33c5aef63988785c541f53bbe35ec6faf66 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 16:10:19 +0300 Subject: [PATCH 01/54] chore(gateway-api-crds): bump to v1.5.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align vendored Gateway API CRDs with the latest stable upstream release (v1.5.1, Oct 2026). TLSRoute graduates to GA in v1.5, still shipped from the experimental channel to keep the superset of optional fields. Cilium 1.19 consumes Gateway API CRDs through standard backward-compatible versioning, so a v1.5 schema is compatible with the Cilium 1.19 controller. Notable additions over the previous v1.2.0 vendoring: - ListenerSet (was XListenerSet / GEP-1713) — shared Gateway with listeners delegated from separate LS objects. Will enable a one-Gateway-per-cluster topology for the upcoming Gateway API work. - BackendTLSPolicy for upstream-to-upstream TLS termination. - XMesh (experimental) for mesh-specific routing hooks. - ValidatingAdmissionPolicy 'safe-upgrades.gateway.networking.k8s.io' plus its binding, shipped by upstream to prevent mixing channels and downgrading CRD versions. Stepping stone for the upcoming Gateway API via Cilium work; isolated from that change so it can be rolled back on its own if any downstream consumer breaks on the newer schema. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/gateway-api-crds/Makefile | 2 +- .../templates/crds-experimental.yaml | 9663 ++++++++++++++--- 2 files changed, 8391 insertions(+), 1274 deletions(-) diff --git a/packages/system/gateway-api-crds/Makefile b/packages/system/gateway-api-crds/Makefile index ad51d591..0d005d8b 100644 --- a/packages/system/gateway-api-crds/Makefile +++ b/packages/system/gateway-api-crds/Makefile @@ -6,4 +6,4 @@ include ../../../hack/package.mk update: rm -rf templates mkdir templates - kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd/experimental?ref=v1.2.0" > templates/crds-experimental.yaml + kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd/experimental?ref=v1.5.1" > templates/crds-experimental.yaml diff --git a/packages/system/gateway-api-crds/templates/crds-experimental.yaml b/packages/system/gateway-api-crds/templates/crds-experimental.yaml index 88b3060e..cf0011eb 100644 --- a/packages/system/gateway-api-crds/templates/crds-experimental.yaml +++ b/packages/system/gateway-api-crds/templates/crds-experimental.yaml @@ -2,508 +2,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null - labels: - gateway.networking.k8s.io/policy: Direct - name: backendlbpolicies.gateway.networking.k8s.io -spec: - group: gateway.networking.k8s.io - names: - categories: - - gateway-api - kind: BackendLBPolicy - listKind: BackendLBPolicyList - plural: backendlbpolicies - shortNames: - - blbpolicy - singular: backendlbpolicy - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha2 - schema: - openAPIV3Schema: - description: |- - BackendLBPolicy provides a way to define load balancing rules - for a backend. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: Spec defines the desired state of BackendLBPolicy. - properties: - sessionPersistence: - description: |- - SessionPersistence defines and configures session persistence - for the backend. - - Support: Extended - properties: - absoluteTimeout: - description: |- - AbsoluteTimeout defines the absolute timeout of the persistent - session. Once the AbsoluteTimeout duration has elapsed, the - session becomes invalid. - - Support: Extended - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - cookieConfig: - description: |- - CookieConfig provides configuration settings that are specific - to cookie-based session persistence. - - Support: Core - properties: - lifetimeType: - default: Session - description: |- - LifetimeType specifies whether the cookie has a permanent or - session-based lifetime. A permanent cookie persists until its - specified expiry time, defined by the Expires or Max-Age cookie - attributes, while a session cookie is deleted when the current - session ends. - - When set to "Permanent", AbsoluteTimeout indicates the - cookie's lifetime via the Expires or Max-Age cookie attributes - and is required. - - When set to "Session", AbsoluteTimeout indicates the - absolute lifetime of the cookie tracked by the gateway and - is optional. - - Support: Core for "Session" type - - Support: Extended for "Permanent" type - enum: - - Permanent - - Session - type: string - type: object - idleTimeout: - description: |- - IdleTimeout defines the idle timeout of the persistent session. - Once the session has been idle for more than the specified - IdleTimeout duration, the session becomes invalid. - - Support: Extended - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - sessionName: - description: |- - SessionName defines the name of the persistent session token - which may be reflected in the cookie or the header. Users - should avoid reusing session names to prevent unintended - consequences, such as rejection or unpredictable behavior. - - Support: Implementation-specific - maxLength: 128 - type: string - type: - default: Cookie - description: |- - Type defines the type of session persistence such as through - the use a header or cookie. Defaults to cookie based session - persistence. - - Support: Core for "Cookie" type - - Support: Extended for "Header" type - enum: - - Cookie - - Header - type: string - type: object - x-kubernetes-validations: - - message: AbsoluteTimeout must be specified when cookie lifetimeType - is Permanent - rule: '!has(self.cookieConfig) || !has(self.cookieConfig.lifetimeType) - || self.cookieConfig.lifetimeType != ''Permanent'' || has(self.absoluteTimeout)' - targetRefs: - description: |- - TargetRef identifies an API object to apply policy to. - Currently, Backends (i.e. Service, ServiceImport, or any - implementation-specific backendRef) are the only valid API - target references. - items: - description: |- - LocalPolicyTargetReference identifies an API object to apply a direct or - inherited policy to. This should be used as part of Policy resources - that can target Gateway API resources. For more information on how this - policy attachment model works, and a sample Policy resource, refer to - the policy attachment documentation for Gateway API. - properties: - group: - description: Group is the group of the target resource. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - description: Kind is kind of the target resource. - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the target resource. - maxLength: 253 - minLength: 1 - type: string - required: - - group - - kind - - name - type: object - maxItems: 16 - minItems: 1 - type: array - x-kubernetes-list-map-keys: - - group - - kind - - name - x-kubernetes-list-type: map - required: - - targetRefs - type: object - status: - description: Status defines the current state of BackendLBPolicy. - properties: - ancestors: - description: |- - Ancestors is a list of ancestor resources (usually Gateways) that are - associated with the policy, and the status of the policy with respect to - each ancestor. When this policy attaches to a parent, the controller that - manages the parent and the ancestors MUST add an entry to this list when - the controller first sees the policy and SHOULD update the entry as - appropriate when the relevant ancestor is modified. - - Note that choosing the relevant ancestor is left to the Policy designers; - an important part of Policy design is designing the right object level at - which to namespace this status. - - Note also that implementations MUST ONLY populate ancestor status for - the Ancestor resources they are responsible for. Implementations MUST - use the ControllerName field to uniquely identify the entries in this list - that they are responsible for. - - Note that to achieve this, the list of PolicyAncestorStatus structs - MUST be treated as a map with a composite key, made up of the AncestorRef - and ControllerName fields combined. - - A maximum of 16 ancestors will be represented in this list. An empty list - means the Policy is not relevant for any ancestors. - - If this slice is full, implementations MUST NOT add further entries. - Instead they MUST consider the policy unimplementable and signal that - on any related resources such as the ancestor that would be referenced - here. For example, if this list was full on BackendTLSPolicy, no - additional Gateways would be able to reference the Service targeted by - the BackendTLSPolicy. - items: - description: |- - PolicyAncestorStatus describes the status of a route with respect to an - associated Ancestor. - - Ancestors refer to objects that are either the Target of a policy or above it - in terms of object hierarchy. For example, if a policy targets a Service, the - Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and - the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most - useful object to place Policy status on, so we recommend that implementations - SHOULD use Gateway as the PolicyAncestorStatus object unless the designers - have a _very_ good reason otherwise. - - In the context of policy attachment, the Ancestor is used to distinguish which - resource results in a distinct application of this policy. For example, if a policy - targets a Service, it may have a distinct result per attached Gateway. - - Policies targeting the same resource may have different effects depending on the - ancestors of those resources. For example, different Gateways targeting the same - Service may have different capabilities, especially if they have different underlying - implementations. - - For example, in BackendTLSPolicy, the Policy attaches to a Service that is - used as a backend in a HTTPRoute that is itself attached to a Gateway. - In this case, the relevant object for status is the Gateway, and that is the - ancestor object referred to in this status. - - Note that a parent is also an ancestor, so for objects where the parent is the - relevant object for status, this struct SHOULD still be used. - - This struct is intended to be used in a slice that's effectively a map, - with a composite key made up of the AncestorRef and the ControllerName. - properties: - ancestorRef: - description: |- - AncestorRef corresponds with a ParentRef in the spec that this - PolicyAncestorStatus struct describes the status of. - properties: - group: - default: gateway.networking.k8s.io - description: |- - Group is the group of the referent. - When unspecified, "gateway.networking.k8s.io" is inferred. - To set the core API group (such as for a "Service" kind referent), - Group must be explicitly set to "" (empty string). - - Support: Core - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Gateway - description: |- - Kind is kind of the referent. - - There are two kinds of parent resources with "Core" support: - - * Gateway (Gateway conformance profile) - * Service (Mesh conformance profile, ClusterIP Services only) - - Support for other resources is Implementation-Specific. - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: |- - Name is the name of the referent. - - Support: Core - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the referent. When unspecified, this refers - to the local namespace of the Route. - - Note that there are specific rules for ParentRefs which cross namespace - boundaries. Cross-namespace references are only valid if they are explicitly - allowed by something in the namespace they are referring to. For example: - Gateway has the AllowedRoutes field, and ReferenceGrant provides a - generic way to enable any other kind of cross-namespace reference. - - - ParentRefs from a Route to a Service in the same namespace are "producer" - routes, which apply default routing rules to inbound connections from - any namespace to the Service. - - ParentRefs from a Route to a Service in a different namespace are - "consumer" routes, and these routing rules are only applied to outbound - connections originating from the same namespace as the Route, for which - the intended destination of the connections are a Service targeted as a - ParentRef of the Route. - - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port is the network port this Route targets. It can be interpreted - differently based on the type of parent resource. - - When the parent resource is a Gateway, this targets all listeners - listening on the specified port that also support this kind of Route(and - select this Route). It's not recommended to set `Port` unless the - networking behaviors specified in a Route must apply to a specific port - as opposed to a listener(s) whose port(s) may be changed. When both Port - and SectionName are specified, the name and port of the selected listener - must match both specified values. - - - When the parent resource is a Service, this targets a specific port in the - Service spec. When both Port (experimental) and SectionName are specified, - the name and port of the selected port must match both specified values. - - - Implementations MAY choose to support other parent resources. - Implementations supporting other types of parent resources MUST clearly - document how/if Port is interpreted. - - For the purpose of status, an attachment is considered successful as - long as the parent resource accepts it partially. For example, Gateway - listeners can restrict which Routes can attach to them by Route kind, - namespace, or hostname. If 1 of 2 Gateway listeners accept attachment - from the referencing Route, the Route MUST be considered successfully - attached. If no Gateway listeners accept attachment from this Route, - the Route MUST be considered detached from the Gateway. - - Support: Extended - format: int32 - maximum: 65535 - minimum: 1 - type: integer - sectionName: - description: |- - SectionName is the name of a section within the target resource. In the - following resources, SectionName is interpreted as the following: - - * Gateway: Listener name. When both Port (experimental) and SectionName - are specified, the name and port of the selected listener must match - both specified values. - * Service: Port name. When both Port (experimental) and SectionName - are specified, the name and port of the selected listener must match - both specified values. - - Implementations MAY choose to support attaching Routes to other resources. - If that is the case, they MUST clearly document how SectionName is - interpreted. - - When unspecified (empty string), this will reference the entire resource. - For the purpose of status, an attachment is considered successful if at - least one section in the parent resource accepts it. For example, Gateway - listeners can restrict which Routes can attach to them by Route kind, - namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from - the referencing Route, the Route MUST be considered successfully - attached. If no Gateway listeners accept attachment from this Route, the - Route MUST be considered detached from the Gateway. - - Support: Core - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - name - type: object - conditions: - description: Conditions describes the status of the Policy with - respect to the given Ancestor. - items: - description: Condition contains details for one aspect of - the current state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, - Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - maxItems: 8 - minItems: 1 - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - controllerName: - description: |- - ControllerName is a domain/path string that indicates the name of the - controller that wrote this status. This corresponds with the - controllerName field on GatewayClass. - - Example: "example.net/gateway-controller". - - The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are - valid Kubernetes names - (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). - - Controllers MUST populate this field when writing status. Controllers should ensure that - entries to status populated with their ControllerName are cleaned up when they are no - longer necessary. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ - type: string - required: - - ancestorRef - - controllerName - type: object - maxItems: 16 - type: array - required: - - ancestors - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 - gateway.networking.k8s.io/channel: experimental - creationTimestamp: null labels: gateway.networking.k8s.io/policy: Direct name: backendtlspolicies.gateway.networking.k8s.io @@ -524,7 +25,7 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date - name: v1alpha3 + name: v1 schema: openAPIV3Schema: description: |- @@ -577,15 +78,62 @@ spec: targetRefs: description: |- TargetRefs identifies an API object to apply the policy to. - Only Services have Extended support. Implementations MAY support - additional objects, with Implementation Specific support. Note that this config applies to the entire referenced resource by default, but this default may change in the future to provide a more granular application of the policy. - Support: Extended for Kubernetes Service + TargetRefs must be _distinct_. This means either that: - Support: Implementation-specific for any other resource + * They select different targets. If this is the case, then targetRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, and `name` must + be unique across all targetRef entries in the BackendTLSPolicy. + * They select different sectionNames in the same target. + + When more than one BackendTLSPolicy selects the same target and + sectionName, implementations MUST determine precedence using the + following criteria, continuing on ties: + + * The older policy by creation timestamp takes precedence. For + example, a policy with a creation timestamp of "2021-07-15 + 01:02:03" MUST be given precedence over a policy with a + creation timestamp of "2021-07-15 01:02:04". + * The policy appearing first in alphabetical order by {namespace}/{name}. + For example, a policy named `foo/bar` is given precedence over a + policy named `foo/baz`. + + For any BackendTLSPolicy that does not take precedence, the + implementation MUST ensure the `Accepted` Condition is set to + `status: False`, with Reason `Conflicted`. + + Implementations SHOULD NOT support more than one targetRef at this + time. Although the API technically allows for this, the current guidance + for conflict resolution and status handling is lacking. Until that can be + clarified in a future release, the safest approach is to support a single + targetRef. + + Support Levels: + + * Extended: Kubernetes Service referenced by HTTPRoute backendRefs. + + * Implementation-Specific: Services not connected via HTTPRoute, and any + other kind of backend. Implementations MAY use BackendTLSPolicy for: + - Services not referenced by any Route (e.g., infrastructure services) + - Gateway feature backends (e.g., ExternalAuth, rate-limiting services) + - Service mesh workload-to-service communication + - Other resource types beyond Service + + Implementations SHOULD aim to ensure that BackendTLSPolicy behavior is consistent, + even outside of the extended HTTPRoute -(backendRef) -> Service path. + They SHOULD clearly document how BackendTLSPolicy is interpreted in these + scenarios, including: + - Which resources beyond Service are supported + - How the policy is discovered and applied + - Any implementation-specific semantics or restrictions + + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. items: description: |- LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a @@ -639,6 +187,21 @@ spec: maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when targetRefs includes + 2 or more references to the same target + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name ? ((!has(p1.sectionName) || p1.sectionName + == '''') == (!has(p2.sectionName) || p2.sectionName == '''')) + : true))' + - message: sectionName must be unique when targetRefs includes 2 or + more references to the same target + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.sectionName) || + p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) validation: description: Validation contains backend TLS validation configuration. properties: @@ -650,11 +213,34 @@ spec: If CACertificateRefs is empty or unspecified, then WellKnownCACertificates must be specified. Only one of CACertificateRefs or WellKnownCACertificates may be specified, - not both. If CACertifcateRefs is empty or unspecified, the configuration for + not both. If CACertificateRefs is empty or unspecified, the configuration for WellKnownCACertificates MUST be honored instead if supported by the implementation. - References to a resource in a different namespace are invalid for the - moment, although we will revisit this in the future. + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a ConfigMap does not contain a key + named `ca.crt`). In this case, the Reason must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this case, the Reason + must be set to `InvalidKind` and the Message of the Condition must explain which + kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace. This may change in future + spec updates. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message must be set for the invalid reference. + + In all cases, the implementation MUST ensure the `ResolvedRefs` Condition on + the BackendTLSPolicy is set to `status: False`, with a Reason and Message + that indicate the cause of the error. Connections using an invalid + CACertificateRef MUST fail, and the client MUST receive an HTTP 5xx error + response. If ALL CACertificateRefs are invalid, the implementation MUST also + ensure the `Accepted` Condition on the BackendTLSPolicy is set to + `status: False`, with a Reason `NoValidCACertificate`. A single CACertificateRef to a Kubernetes ConfigMap kind has "Core" support. Implementations MAY choose to support attaching multiple certificates to @@ -663,8 +249,8 @@ spec: Support: Core - An optional single reference to a Kubernetes ConfigMap, with the CA certificate in a key named `ca.crt`. - Support: Implementation-specific (More than one reference, or other kinds - of resources). + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. items: description: |- LocalObjectReference identifies an API object within the namespace of the @@ -702,15 +288,18 @@ spec: type: object maxItems: 8 type: array + x-kubernetes-list-type: atomic hostname: description: |- Hostname is used for two purposes in the connection between Gateways and backends: 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). - 2. If SubjectAltNames is not specified, Hostname MUST be used for - authentication and MUST match the certificate served by the matching - backend. + 2. Hostname MUST be used for authentication and MUST match the certificate + served by the matching backend, unless SubjectAltNames is specified. + 3. If SubjectAltNames are specified, Hostname can be used for certificate selection + but MUST NOT be used for authentication. If you want to use the value + of the Hostname field for authentication, you MUST add it to the SubjectAltNames list. Support: Core maxLength: 253 @@ -720,10 +309,10 @@ spec: subjectAltNames: description: |- SubjectAltNames contains one or more Subject Alternative Names. - When specified, the certificate served from the backend MUST have at least one - Subject Alternate Name matching one of the specified SubjectAltNames. + When specified the certificate served from the backend MUST + have at least one Subject Alternate Name matching one of the specified SubjectAltNames. - Support: Core + Support: Extended items: description: SubjectAltName represents Subject Alternative Name. properties: @@ -780,21 +369,31 @@ spec: "")' maxItems: 5 type: array + x-kubernetes-list-type: atomic wellKnownCACertificates: description: |- - WellKnownCACertificates specifies whether system CA certificates may be used in - the TLS handshake between the gateway and backend pod. + WellKnownCACertificates specifies whether a well-known set of CA certificates + may be used in the TLS handshake between the gateway and backend pod. If WellKnownCACertificates is unspecified or empty (""), then CACertificateRefs must be specified with at least one entry for a valid configuration. Only one of - CACertificateRefs or WellKnownCACertificates may be specified, not both. If an - implementation does not support the WellKnownCACertificates field or the value - supplied is not supported, the Status Conditions on the Policy MUST be - updated to include an Accepted: False Condition with Reason: Invalid. + CACertificateRefs or WellKnownCACertificates may be specified, not both. + If an implementation does not support the WellKnownCACertificates field, or + the supplied value is not recognized, the implementation MUST ensure the + `Accepted` Condition on the BackendTLSPolicy is set to `status: False`, with + a Reason `Invalid`. + + Valid values include: + * "System" - indicates that well-known system CA certificates should be used. + + Implementations MAY define their own sets of CA certificates. Such definitions + MUST use an implementation-specific, prefixed name, such as + `mycompany.com/my-custom-ca-certificates`. Support: Implementation-specific - enum: - - System + maxLength: 253 + minLength: 1 + pattern: ^(System|([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]))$ type: string required: - hostname @@ -1101,10 +700,12 @@ spec: type: string required: - ancestorRef + - conditions - controllerName type: object maxItems: 16 type: array + x-kubernetes-list-type: atomic required: - ancestors type: object @@ -1115,6 +716,700 @@ spec: storage: true subresources: status: {} + - deprecated: true + deprecationWarning: The v1alpha3 version of BackendTLSPolicy has been deprecated + and will be removed in a future release of the API. Please upgrade to v1. + name: v1alpha3 + schema: + openAPIV3Schema: + description: |- + BackendTLSPolicy provides a way to configure how a Gateway + connects to a Backend via TLS. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of BackendTLSPolicy. + properties: + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + targetRefs: + description: |- + TargetRefs identifies an API object to apply the policy to. + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + + TargetRefs must be _distinct_. This means either that: + + * They select different targets. If this is the case, then targetRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, and `name` must + be unique across all targetRef entries in the BackendTLSPolicy. + * They select different sectionNames in the same target. + + When more than one BackendTLSPolicy selects the same target and + sectionName, implementations MUST determine precedence using the + following criteria, continuing on ties: + + * The older policy by creation timestamp takes precedence. For + example, a policy with a creation timestamp of "2021-07-15 + 01:02:03" MUST be given precedence over a policy with a + creation timestamp of "2021-07-15 01:02:04". + * The policy appearing first in alphabetical order by {namespace}/{name}. + For example, a policy named `foo/bar` is given precedence over a + policy named `foo/baz`. + + For any BackendTLSPolicy that does not take precedence, the + implementation MUST ensure the `Accepted` Condition is set to + `status: False`, with Reason `Conflicted`. + + Implementations SHOULD NOT support more than one targetRef at this + time. Although the API technically allows for this, the current guidance + for conflict resolution and status handling is lacking. Until that can be + clarified in a future release, the safest approach is to support a single + targetRef. + + Support Levels: + + * Extended: Kubernetes Service referenced by HTTPRoute backendRefs. + + * Implementation-Specific: Services not connected via HTTPRoute, and any + other kind of backend. Implementations MAY use BackendTLSPolicy for: + - Services not referenced by any Route (e.g., infrastructure services) + - Gateway feature backends (e.g., ExternalAuth, rate-limiting services) + - Service mesh workload-to-service communication + - Other resource types beyond Service + + Implementations SHOULD aim to ensure that BackendTLSPolicy behavior is consistent, + even outside of the extended HTTPRoute -(backendRef) -> Service path. + They SHOULD clearly document how BackendTLSPolicy is interpreted in these + scenarios, including: + - Which resources beyond Service are supported + - How the policy is discovered and applied + - Any implementation-specific semantics or restrictions + + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + items: + description: |- + LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a + direct policy to. This should be used as part of Policy resources that can + target single resources. For more information on how this policy attachment + mode works, and a sample Policy resource, refer to the policy attachment + documentation for Gateway API. + + Note: This should only be used for direct policy attachment when references + to SectionName are actually needed. In all other cases, + LocalPolicyTargetReference should be used. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when targetRefs includes + 2 or more references to the same target + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name ? ((!has(p1.sectionName) || p1.sectionName + == '''') == (!has(p2.sectionName) || p2.sectionName == '''')) + : true))' + - message: sectionName must be unique when targetRefs includes 2 or + more references to the same target + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.sectionName) || + p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + validation: + description: Validation contains backend TLS validation configuration. + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes objects that + contain a PEM-encoded TLS CA certificate bundle, which is used to + validate a TLS handshake between the Gateway and backend Pod. + + If CACertificateRefs is empty or unspecified, then WellKnownCACertificates must be + specified. Only one of CACertificateRefs or WellKnownCACertificates may be specified, + not both. If CACertificateRefs is empty or unspecified, the configuration for + WellKnownCACertificates MUST be honored instead if supported by the implementation. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a ConfigMap does not contain a key + named `ca.crt`). In this case, the Reason must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this case, the Reason + must be set to `InvalidKind` and the Message of the Condition must explain which + kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace. This may change in future + spec updates. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message must be set for the invalid reference. + + In all cases, the implementation MUST ensure the `ResolvedRefs` Condition on + the BackendTLSPolicy is set to `status: False`, with a Reason and Message + that indicate the cause of the error. Connections using an invalid + CACertificateRef MUST fail, and the client MUST receive an HTTP 5xx error + response. If ALL CACertificateRefs are invalid, the implementation MUST also + ensure the `Accepted` Condition on the BackendTLSPolicy is set to + `status: False`, with a Reason `NoValidCACertificate`. + + A single CACertificateRef to a Kubernetes ConfigMap kind has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a backend, but this behavior is implementation-specific. + + Support: Core - An optional single reference to a Kubernetes ConfigMap, + with the CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + LocalObjectReference identifies an API object within the namespace of the + referrer. + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example "HTTPRoute" + or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + hostname: + description: |- + Hostname is used for two purposes in the connection between Gateways and + backends: + + 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). + 2. Hostname MUST be used for authentication and MUST match the certificate + served by the matching backend, unless SubjectAltNames is specified. + 3. If SubjectAltNames are specified, Hostname can be used for certificate selection + but MUST NOT be used for authentication. If you want to use the value + of the Hostname field for authentication, you MUST add it to the SubjectAltNames list. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + subjectAltNames: + description: |- + SubjectAltNames contains one or more Subject Alternative Names. + When specified the certificate served from the backend MUST + have at least one Subject Alternate Name matching one of the specified SubjectAltNames. + + Support: Extended + items: + description: SubjectAltName represents Subject Alternative Name. + properties: + hostname: + description: |- + Hostname contains Subject Alternative Name specified in DNS name format. + Required when Type is set to Hostname, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: + description: |- + Type determines the format of the Subject Alternative Name. Always required. + + Support: Core + enum: + - Hostname + - URI + type: string + uri: + description: |- + URI contains Subject Alternative Name specified in a full URI format. + It MUST include both a scheme (e.g., "http" or "ftp") and a scheme-specific-part. + Common values include SPIFFE IDs like "spiffe://mycluster.example.com/ns/myns/sa/svc1sa". + Required when Type is set to URI, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\?([^#]*))?(#(.*))? + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: SubjectAltName element must contain Hostname, if + Type is set to Hostname + rule: '!(self.type == "Hostname" && (!has(self.hostname) || + self.hostname == ""))' + - message: SubjectAltName element must not contain Hostname, + if Type is not set to Hostname + rule: '!(self.type != "Hostname" && has(self.hostname) && + self.hostname != "")' + - message: SubjectAltName element must contain URI, if Type + is set to URI + rule: '!(self.type == "URI" && (!has(self.uri) || self.uri + == ""))' + - message: SubjectAltName element must not contain URI, if Type + is not set to URI + rule: '!(self.type != "URI" && has(self.uri) && self.uri != + "")' + maxItems: 5 + type: array + x-kubernetes-list-type: atomic + wellKnownCACertificates: + description: |- + WellKnownCACertificates specifies whether a well-known set of CA certificates + may be used in the TLS handshake between the gateway and backend pod. + + If WellKnownCACertificates is unspecified or empty (""), then CACertificateRefs + must be specified with at least one entry for a valid configuration. Only one of + CACertificateRefs or WellKnownCACertificates may be specified, not both. + If an implementation does not support the WellKnownCACertificates field, or + the supplied value is not recognized, the implementation MUST ensure the + `Accepted` Condition on the BackendTLSPolicy is set to `status: False`, with + a Reason `Invalid`. + + Valid values include: + * "System" - indicates that well-known system CA certificates should be used. + + Implementations MAY define their own sets of CA certificates. Such definitions + MUST use an implementation-specific, prefixed name, such as + `mycompany.com/my-custom-ca-certificates`. + + Support: Implementation-specific + maxLength: 253 + minLength: 1 + pattern: ^(System|([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]))$ + type: string + required: + - hostname + type: object + x-kubernetes-validations: + - message: must not contain both CACertificateRefs and WellKnownCACertificates + rule: '!(has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 && has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "")' + - message: must specify either CACertificateRefs or WellKnownCACertificates + rule: (has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 || has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "") + required: + - targetRefs + - validation + type: object + status: + description: Status defines the current state of BackendTLSPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - conditions + - controllerName + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + required: + - ancestors + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} status: acceptedNames: kind: "" @@ -1126,10 +1421,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: gatewayclasses.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -1362,7 +1656,7 @@ spec: - type x-kubernetes-list-type: map supportedFeatures: - description: | + description: |- SupportedFeatures is the set of features the GatewayClass support. It MUST be sorted in ascending alphabetical order by the Name key. items: @@ -1606,7 +1900,7 @@ spec: - type x-kubernetes-list-type: map supportedFeatures: - description: | + description: |- SupportedFeatures is the set of features the GatewayClass support. It MUST be sorted in ascending alphabetical order by the Name key. items: @@ -1643,10 +1937,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: gateways.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -1702,11 +1995,11 @@ spec: description: Spec defines the desired state of Gateway. properties: addresses: - description: |+ + description: |- Addresses requested for this Gateway. This is optional and behavior can depend on the implementation. If a value is set in the spec and the requested address is invalid or unavailable, the implementation MUST - indicate this in the associated entry in GatewayStatus.Addresses. + indicate this in an associated entry in GatewayStatus.Conditions. The Addresses field represents a request for the address(es) on the "outside of the Gateway", that traffic bound for this Gateway will use. @@ -1723,10 +2016,9 @@ spec: GatewayStatus.Addresses. Support: Extended - items: - description: GatewayAddress describes an address that can be bound - to a Gateway. + description: GatewaySpecAddress describes an address that can be + bound to a Gateway. oneOf: - properties: type: @@ -1751,96 +2043,137 @@ spec: type: string value: description: |- - Value of the address. The validity of the values will depend - on the type and support by the controller. + When a value is unspecified, an implementation SHOULD automatically + assign an address matching the requested type if possible. + + If an implementation does not support an empty value, they MUST set the + "Programmed" condition in status to False with a reason of "AddressNotAssigned". Examples: `1.2.3.4`, `128::1`, `my-ip-address`. maxLength: 253 - minLength: 1 type: string - required: - - value type: object x-kubernetes-validations: - - message: Hostname value must only contain valid characters (matching - ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) - rule: 'self.type == ''Hostname'' ? self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"""): + - message: Hostname value must be empty or contain only valid characters + (matching ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? (!has(self.value) || self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$""")): true' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: IPAddress values must be unique - rule: 'self.all(a1, a1.type == ''IPAddress'' ? self.exists_one(a2, - a2.type == a1.type && a2.value == a1.value) : true )' + rule: 'self.all(a1, a1.type == ''IPAddress'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' - message: Hostname values must be unique - rule: 'self.all(a1, a1.type == ''Hostname'' ? self.exists_one(a2, - a2.type == a1.type && a2.value == a1.value) : true )' - backendTLS: - description: |+ - BackendTLS configures TLS settings for when this Gateway is connecting to - backends with TLS. - - Support: Core - + rule: 'self.all(a1, a1.type == ''Hostname'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + allowedListeners: + description: |- + AllowedListeners defines which ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. properties: - clientCertificateRef: - description: |+ - ClientCertificateRef is a reference to an object that contains a Client - Certificate and the associated private key. - - References to a resource in different namespace are invalid UNLESS there - is a ReferenceGrant in the target namespace that allows the certificate - to be attached. If a ReferenceGrant does not allow this reference, the - "ResolvedRefs" condition MUST be set to False for this listener with the - "RefNotPermitted" reason. - - ClientCertificateRef can reference to standard Kubernetes resources, i.e. - Secret, or implementation-specific custom resources. - - This setting can be overridden on the service level by use of BackendTLSPolicy. - - Support: Core - + namespaces: + default: + from: None + description: |- + Namespaces defines which namespaces ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. properties: - group: - default: "" + from: + default: None description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + From indicates where ListenerSets can attach to this Gateway. Possible + values are: + + * Same: Only ListenerSets in the same namespace may be attached to this Gateway. + * Selector: ListenerSets in namespaces selected by the selector may be attached to this Gateway. + * All: ListenerSets in all namespaces may be attached to this Gateway. + * None: Only listeners defined in the Gateway's spec are allowed + + The default value None + enum: + - All + - Selector + - Same + - None type: string - kind: - default: Secret - description: Kind is kind of the referent. For example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: + selector: description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name + Selector must be specified when From is set to "Selector". In that case, + only ListenerSets in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object type: object + defaultScope: + description: |- + DefaultScope, when set, configures the Gateway as a default Gateway, + meaning it will dynamically and implicitly have Routes (e.g. HTTPRoute) + attached to it, according to the scope configured here. + + If unset (the default) or set to None, the Gateway will not act as a + default Gateway; if set, the Gateway will claim any Route with a + matching scope set in its UseDefaultGateway field, subject to the usual + rules about which routes the Gateway can attach to. + + Think carefully before using this functionality! While the normal rules + about which Route can apply are still enforced, it is simply easier for + the wrong Route to be accidentally attached to this Gateway in this + configuration. If the Gateway operator is not also the operator in + control of the scope (e.g. namespace) with tight controls and checks on + what kind of workloads and Routes get added in that scope, we strongly + recommend not using this just because it seems convenient, and instead + stick to direct Route attachment. + enum: + - All + - None + type: string gatewayClassName: description: |- GatewayClassName used for this Gateway. This is the name of a @@ -1935,6 +2268,11 @@ spec: the merging behavior is implementation specific. It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Gateway SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + Support: Implementation-specific properties: group: @@ -1965,6 +2303,8 @@ spec: logical endpoints that are bound on this Gateway's addresses. At least one Listener MUST be specified. + ## Distinct Listeners + Each Listener in a set of Listeners (for example, in a single Gateway) MUST be _distinct_, in that a traffic flow MUST be able to be assigned to exactly one listener. (This section uses "set of Listeners" rather than @@ -1976,55 +2316,76 @@ spec: combination of Port, Protocol, and, if supported by the protocol, Hostname. Some combinations of port, protocol, and TLS settings are considered - Core support and MUST be supported by implementations based on their - targeted conformance profile: + Core support and MUST be supported by implementations based on the objects + they support: - HTTP Profile + HTTPRoute 1. HTTPRoute, Port: 80, Protocol: HTTP 2. HTTPRoute, Port: 443, Protocol: HTTPS, TLS Mode: Terminate, TLS keypair provided - TLS Profile + TLSRoute 1. TLSRoute, Port: 443, Protocol: TLS, TLS Mode: Passthrough "Distinct" Listeners have the following property: - The implementation can match inbound requests to a single distinct - Listener. When multiple Listeners share values for fields (for + **The implementation can match inbound requests to a single distinct + Listener**. + + When multiple Listeners share values for fields (for example, two Listeners with the same Port value), the implementation can match requests to only one of the Listeners using other Listener fields. - For example, the following Listener scenarios are distinct: + When multiple listeners have the same value for the Protocol field, then + each of the Listeners with matching Protocol values MUST have different + values for other fields. - 1. Multiple Listeners with the same Port that all use the "HTTP" - Protocol that all have unique Hostname values. - 2. Multiple Listeners with the same Port that use either the "HTTPS" or - "TLS" Protocol that all have unique Hostname values. - 3. A mixture of "TCP" and "UDP" Protocol Listeners, where no Listener - with the same Protocol has the same Port value. + The set of fields that MUST be different for a Listener differs per protocol. + The following rules define the rules for what fields MUST be considered for + Listeners to be distinct with each protocol currently defined in the + Gateway API spec. - Some fields in the Listener struct have possible values that affect - whether the Listener is distinct. Hostname is particularly relevant - for HTTP or HTTPS protocols. + The set of listeners that all share a protocol value MUST have _different_ + values for _at least one_ of these fields to be distinct: - When using the Hostname value to select between same-Port, same-Protocol - Listeners, the Hostname value must be different on each Listener for the - Listener to be distinct. + * **HTTP, HTTPS, TLS**: Port, Hostname + * **TCP, UDP**: Port - When the Listeners are distinct based on Hostname, inbound request + One **very** important rule to call out involves what happens when an + implementation: + + * Supports TCP protocol Listeners, as well as HTTP, HTTPS, or TLS protocol + Listeners, and + * sees HTTP, HTTPS, or TLS protocols with the same `port` as one with TCP + Protocol. + + In this case all the Listeners that share a port with the + TCP Listener are not distinct and so MUST NOT be accepted. + + If an implementation does not support TCP Protocol Listeners, then the + previous rule does not apply, and the TCP Listeners SHOULD NOT be + accepted. + + Note that the `tls` field is not used for determining if a listener is distinct, because + Listeners that _only_ differ on TLS config will still conflict in all cases. + + ### Listeners that are distinct only by Hostname + + When the Listeners are distinct based only on Hostname, inbound request hostnames MUST match from the most specific to least specific Hostname values to choose the correct Listener and its associated set of Routes. - Exact matches must be processed before wildcard matches, and wildcard - matches must be processed before fallback (empty Hostname value) + Exact matches MUST be processed before wildcard matches, and wildcard + matches MUST be processed before fallback (empty Hostname value) matches. For example, `"foo.example.com"` takes precedence over `"*.example.com"`, and `"*.example.com"` takes precedence over `""`. Additionally, if there are multiple wildcard entries, more specific wildcard entries must be processed before less specific wildcard entries. For example, `"*.foo.example.com"` takes precedence over `"*.example.com"`. + The precise definition here is that the higher the number of dots in the hostname to the right of the wildcard character, the higher the precedence. @@ -2032,18 +2393,26 @@ spec: the left, however, so `"*.example.com"` will match both `"foo.bar.example.com"` _and_ `"bar.example.com"`. + ## Handling indistinct Listeners + If a set of Listeners contains Listeners that are not distinct, then those - Listeners are Conflicted, and the implementation MUST set the "Conflicted" + Listeners are _Conflicted_, and the implementation MUST set the "Conflicted" condition in the Listener Status to "True". + The words "indistinct" and "conflicted" are considered equivalent for the + purpose of this documentation. + Implementations MAY choose to accept a Gateway with some Conflicted Listeners only if they only accept the partial Listener set that contains - no Conflicted Listeners. To put this another way, implementations may - accept a partial Listener set only if they throw out *all* the conflicting - Listeners. No picking one of the conflicting listeners as the winner. - This also means that the Gateway must have at least one non-conflicting - Listener in this case, otherwise it violates the requirement that at - least one Listener must be present. + no Conflicted Listeners. + + Specifically, an implementation MAY accept a partial Listener set subject to + the following rules: + + * The implementation MUST NOT pick one conflicting Listener as the winner. + ALL indistinct Listeners must not be accepted for processing. + * At least one distinct Listener MUST be present, or else the Gateway effectively + contains _no_ Listeners, and must be rejected from processing as a whole. The implementation MUST set a "ListenersNotValid" condition on the Gateway Status when the Gateway contains Conflicted Listeners whether or @@ -2052,7 +2421,25 @@ spec: Accepted. Additionally, the Listener status for those listeners SHOULD indicate which Listeners are conflicted and not Accepted. - A Gateway's Listeners are considered "compatible" if: + ## General Listener behavior + + Note that, for all distinct Listeners, requests SHOULD match at most one Listener. + For example, if Listeners are defined for "foo.example.com" and "*.example.com", a + request to "foo.example.com" SHOULD only be routed using routes attached + to the "foo.example.com" Listener (and not the "*.example.com" Listener). + + This concept is known as "Listener Isolation", and it is an Extended feature + of Gateway API. Implementations that do not support Listener Isolation MUST + clearly document this, and MUST NOT claim support for the + `GatewayHTTPListenerIsolation` feature. + + Implementations that _do_ support Listener Isolation SHOULD claim support + for the Extended `GatewayHTTPListenerIsolation` feature and pass the associated + conformance tests. + + ## Compatible Listeners + + A Gateway's Listeners are considered _compatible_ if: 1. They are distinct. 2. The implementation can serve them in compliance with the Addresses @@ -2067,16 +2454,11 @@ spec: on the same address, or cannot mix HTTPS and generic TLS listens on the same port would not consider those cases compatible, even though they are distinct. - Note that requests SHOULD match at most one Listener. For example, if - Listeners are defined for "foo.example.com" and "*.example.com", a - request to "foo.example.com" SHOULD only be routed using routes attached - to the "foo.example.com" Listener (and not the "*.example.com" Listener). - This concept is known as "Listener Isolation". Implementations that do - not support Listener Isolation MUST clearly document this. - Implementations MAY merge separate Gateways onto a single set of Addresses if all Listeners across all Gateways are compatible. + In a future release the MinItems=1 requirement MAY be dropped. + Support: Core items: description: |- @@ -2147,6 +2529,7 @@ spec: type: object maxItems: 8 type: array + x-kubernetes-list-type: atomic namespaces: default: from: Same @@ -2238,10 +2621,31 @@ spec: * TLS: The Listener Hostname MUST match the SNI. * HTTP: The Listener Hostname MUST match the Host header of the request. - * HTTPS: The Listener Hostname SHOULD match at both the TLS and HTTP - protocol layers as described above. If an implementation does not - ensure that both the SNI and Host header match the Listener hostname, - it MUST clearly document that. + * HTTPS: The Listener Hostname SHOULD match both the SNI and Host header. + Note that this does not require the SNI and Host header to be the same. + The semantics of this are described in more detail below. + + To ensure security, Section 11.1 of RFC-6066 emphasizes that server + implementations that rely on SNI hostname matching MUST also verify + hostnames within the application protocol. + + Section 9.1.2 of RFC-7540 provides a mechanism for servers to reject the + reuse of a connection by responding with the HTTP 421 Misdirected Request + status code. This indicates that the origin server has rejected the + request because it appears to have been misdirected. + + To detect misdirected requests, Gateways SHOULD match the authority of + the requests with all the SNI hostname(s) configured across all the + Gateway Listeners on the same port and protocol: + + * If another Listener has an exact match or more specific wildcard entry, + the Gateway SHOULD return a 421. + * If the current Listener (selected by SNI matching during ClientHello) + does not match the Host: + * If another Listener does match the Host, the Gateway SHOULD return a + 421. + * If no other Listener matches the Host, the Gateway MUST return a + 404. For HTTPRoute and TLSRoute resources, there is an interaction with the `spec.hostnames` array. When both listener and route specify hostnames, @@ -2293,7 +2697,7 @@ spec: the Protocol field is "HTTPS" or "TLS". It is invalid to set this field if the Protocol field is "HTTP", "TCP", or "UDP". - The association of SNIs to Certificate defined in GatewayTLSConfig is + The association of SNIs to Certificate defined in ListenerTLSConfig is defined based on the Hostname field for this listener. The GatewayClass MUST use the longest matching SNI out of all @@ -2380,94 +2784,7 @@ spec: type: object maxItems: 64 type: array - frontendValidation: - description: |+ - FrontendValidation holds configuration information for validating the frontend (client). - Setting this field will require clients to send a client certificate - required for validation during the TLS handshake. In browsers this may result in a dialog appearing - that requests a user to specify the client certificate. - The maximum depth of a certificate chain accepted in verification is Implementation specific. - - Support: Extended - - properties: - caCertificateRefs: - description: |- - CACertificateRefs contains one or more references to - Kubernetes objects that contain TLS certificates of - the Certificate Authorities that can be used - as a trust anchor to validate the certificates presented by the client. - - A single CA certificate reference to a Kubernetes ConfigMap - has "Core" support. - Implementations MAY choose to support attaching multiple CA certificates to - a Listener, but this behavior is implementation-specific. - - Support: Core - A single reference to a Kubernetes ConfigMap - with the CA certificate in a key named `ca.crt`. - - Support: Implementation-specific (More than one reference, or other kinds - of resources). - - References to a resource in a different namespace are invalid UNLESS there - is a ReferenceGrant in the target namespace that allows the certificate - to be attached. If a ReferenceGrant does not allow this reference, the - "ResolvedRefs" condition MUST be set to False for this listener with the - "RefNotPermitted" reason. - items: - description: |- - ObjectReference identifies an API object including its namespace. - - The API object must be valid in the cluster; the Group and Kind must - be registered in the cluster for this reference to be valid. - - References to objects with invalid Group and Kind are not valid, and must - be rejected by the implementation, with appropriate Conditions set - on the containing object. - properties: - group: - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - description: Kind is kind of the referent. For - example "ConfigMap" or "Service". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - group - - kind - - name - type: object - maxItems: 8 - minItems: 1 - type: array - type: object + x-kubernetes-list-type: atomic mode: default: Terminate description: |- @@ -2536,6 +2853,9 @@ spec: - message: tls mode must be Terminate for protocol HTTPS rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' - message: hostname must not be specified for protocols ['TCP', 'UDP'] rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) || l.hostname == '''') : true)' @@ -2546,6 +2866,421 @@ spec: rule: 'self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname == l2.hostname : !has(l1.hostname) && !has(l2.hostname))))' + tls: + description: |- + TLS specifies frontend and backend tls configuration for entire gateway. + + Support: Extended + properties: + backend: + description: |- + Backend describes TLS configuration for gateway when connecting + to backends. + + Note that this contains only details for the Gateway as a TLS client, + and does _not_ imply behavior about how to choose which backend should + get a TLS connection. That is determined by the presence of a BackendTLSPolicy. + + Support: Core + properties: + clientCertificateRef: + description: |- + ClientCertificateRef references an object that contains a client certificate + and its associated private key. It can reference standard Kubernetes resources, + i.e., Secret, or implementation-specific custom resources. + + A ClientCertificateRef is considered invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a Secret does not contain the keys + named `tls.crt` and `tls.key`). In this case, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `InvalidClientCertificateRef` + and the Message of the Condition MUST indicate why the reference is invalid. + + * It refers to a resource in another namespace UNLESS there is a ReferenceGrant + in the target namespace that allows the certificate to be attached. + If a ReferenceGrant does not allow this reference, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message MUST be set. + + Support: Core - Reference to a Kubernetes TLS Secret (with the type `kubernetes.io/tls`). + Support: Implementation-specific - Other resource kinds or Secrets with a + different type (e.g., `Opaque`). + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + type: object + frontend: + description: |- + Frontend describes TLS config when client connects to Gateway. + Support: Core + properties: + default: + description: |- + Default specifies the default client certificate validation configuration + for all Listeners handling HTTPS traffic, unless a per-port configuration + is defined. + + support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + perPort: + description: |- + PerPort specifies tls configuration assigned per port. + Per port configuration is optional. Once set this configuration overrides + the default configuration for all Listeners handling HTTPS traffic + that match this port. + Each override port requires a unique TLS configuration. + + support: Core + items: + properties: + port: + description: |- + The Port indicates the Port Number to which the TLS configuration will be + applied. This configuration will be applied to all Listeners handling HTTPS + traffic that match this port. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + tls: + description: |- + TLS store the configuration that will be applied to all Listeners handling + HTTPS traffic and matching given port. + + Support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + For example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + required: + - port + - tls + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - port + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: Port for TLS configuration must be unique within + the Gateway + rule: self.all(t1, self.exists_one(t2, t1.port == t2.port)) + required: + - default + type: object + type: object required: - gatewayClassName - listeners @@ -2566,7 +3301,7 @@ spec: description: Status defines the current state of Gateway. properties: addresses: - description: |+ + description: |- Addresses lists the network addresses that have been bound to the Gateway. @@ -2576,7 +3311,6 @@ spec: * no addresses are specified, all addresses are dynamically assigned * a combination of specified and dynamic addresses are assigned * a specified address was unusable (e.g. already in use) - items: description: GatewayStatusAddress describes a network address that is bound to a Gateway. @@ -2621,6 +3355,21 @@ spec: true' maxItems: 16 type: array + x-kubernetes-list-type: atomic + attachedListenerSets: + description: |- + AttachedListenerSets represents the total number of ListenerSets that have been + successfully attached to this Gateway. + + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + + Uses for this field include troubleshooting AttachedListenerSets attachment and + measuring blast radius/impact of changes to a Gateway. + format: int32 + type: integer conditions: default: - lastTransitionTime: "1970-01-01T00:00:00Z" @@ -2725,8 +3474,11 @@ spec: attachment semantics can be found in the documentation on the various Route kinds ParentRefs fields). Listener or Route status does not impact successful attachment, i.e. the AttachedRoutes field count MUST be set - for Listeners with condition Accepted: false and MUST count successfully - attached Routes that may themselves have Accepted: false conditions. + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. Uses for this field include troubleshooting Route attachment and measuring blast radius/impact of changes to a Listener. @@ -2805,7 +3557,7 @@ spec: supportedKinds: description: |- SupportedKinds is the list indicating the Kinds supported by this - listener. This MUST represent the kinds an implementation supports for + listener. This MUST represent the kinds supported by an implementation for that Listener configuration. If kinds are specified in Spec that are not supported, they MUST NOT @@ -2834,11 +3586,11 @@ spec: type: object maxItems: 8 type: array + x-kubernetes-list-type: atomic required: - attachedRoutes - conditions - name - - supportedKinds type: object maxItems: 64 type: array @@ -2894,11 +3646,11 @@ spec: description: Spec defines the desired state of Gateway. properties: addresses: - description: |+ + description: |- Addresses requested for this Gateway. This is optional and behavior can depend on the implementation. If a value is set in the spec and the requested address is invalid or unavailable, the implementation MUST - indicate this in the associated entry in GatewayStatus.Addresses. + indicate this in an associated entry in GatewayStatus.Conditions. The Addresses field represents a request for the address(es) on the "outside of the Gateway", that traffic bound for this Gateway will use. @@ -2915,10 +3667,9 @@ spec: GatewayStatus.Addresses. Support: Extended - items: - description: GatewayAddress describes an address that can be bound - to a Gateway. + description: GatewaySpecAddress describes an address that can be + bound to a Gateway. oneOf: - properties: type: @@ -2943,96 +3694,137 @@ spec: type: string value: description: |- - Value of the address. The validity of the values will depend - on the type and support by the controller. + When a value is unspecified, an implementation SHOULD automatically + assign an address matching the requested type if possible. + + If an implementation does not support an empty value, they MUST set the + "Programmed" condition in status to False with a reason of "AddressNotAssigned". Examples: `1.2.3.4`, `128::1`, `my-ip-address`. maxLength: 253 - minLength: 1 type: string - required: - - value type: object x-kubernetes-validations: - - message: Hostname value must only contain valid characters (matching - ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) - rule: 'self.type == ''Hostname'' ? self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"""): + - message: Hostname value must be empty or contain only valid characters + (matching ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? (!has(self.value) || self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$""")): true' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: IPAddress values must be unique - rule: 'self.all(a1, a1.type == ''IPAddress'' ? self.exists_one(a2, - a2.type == a1.type && a2.value == a1.value) : true )' + rule: 'self.all(a1, a1.type == ''IPAddress'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' - message: Hostname values must be unique - rule: 'self.all(a1, a1.type == ''Hostname'' ? self.exists_one(a2, - a2.type == a1.type && a2.value == a1.value) : true )' - backendTLS: - description: |+ - BackendTLS configures TLS settings for when this Gateway is connecting to - backends with TLS. - - Support: Core - + rule: 'self.all(a1, a1.type == ''Hostname'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + allowedListeners: + description: |- + AllowedListeners defines which ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. properties: - clientCertificateRef: - description: |+ - ClientCertificateRef is a reference to an object that contains a Client - Certificate and the associated private key. - - References to a resource in different namespace are invalid UNLESS there - is a ReferenceGrant in the target namespace that allows the certificate - to be attached. If a ReferenceGrant does not allow this reference, the - "ResolvedRefs" condition MUST be set to False for this listener with the - "RefNotPermitted" reason. - - ClientCertificateRef can reference to standard Kubernetes resources, i.e. - Secret, or implementation-specific custom resources. - - This setting can be overridden on the service level by use of BackendTLSPolicy. - - Support: Core - + namespaces: + default: + from: None + description: |- + Namespaces defines which namespaces ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. properties: - group: - default: "" + from: + default: None description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + From indicates where ListenerSets can attach to this Gateway. Possible + values are: + + * Same: Only ListenerSets in the same namespace may be attached to this Gateway. + * Selector: ListenerSets in namespaces selected by the selector may be attached to this Gateway. + * All: ListenerSets in all namespaces may be attached to this Gateway. + * None: Only listeners defined in the Gateway's spec are allowed + + The default value None + enum: + - All + - Selector + - Same + - None type: string - kind: - default: Secret - description: Kind is kind of the referent. For example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: + selector: description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name + Selector must be specified when From is set to "Selector". In that case, + only ListenerSets in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object type: object + defaultScope: + description: |- + DefaultScope, when set, configures the Gateway as a default Gateway, + meaning it will dynamically and implicitly have Routes (e.g. HTTPRoute) + attached to it, according to the scope configured here. + + If unset (the default) or set to None, the Gateway will not act as a + default Gateway; if set, the Gateway will claim any Route with a + matching scope set in its UseDefaultGateway field, subject to the usual + rules about which routes the Gateway can attach to. + + Think carefully before using this functionality! While the normal rules + about which Route can apply are still enforced, it is simply easier for + the wrong Route to be accidentally attached to this Gateway in this + configuration. If the Gateway operator is not also the operator in + control of the scope (e.g. namespace) with tight controls and checks on + what kind of workloads and Routes get added in that scope, we strongly + recommend not using this just because it seems convenient, and instead + stick to direct Route attachment. + enum: + - All + - None + type: string gatewayClassName: description: |- GatewayClassName used for this Gateway. This is the name of a @@ -3127,6 +3919,11 @@ spec: the merging behavior is implementation specific. It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Gateway SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + Support: Implementation-specific properties: group: @@ -3157,6 +3954,8 @@ spec: logical endpoints that are bound on this Gateway's addresses. At least one Listener MUST be specified. + ## Distinct Listeners + Each Listener in a set of Listeners (for example, in a single Gateway) MUST be _distinct_, in that a traffic flow MUST be able to be assigned to exactly one listener. (This section uses "set of Listeners" rather than @@ -3168,55 +3967,76 @@ spec: combination of Port, Protocol, and, if supported by the protocol, Hostname. Some combinations of port, protocol, and TLS settings are considered - Core support and MUST be supported by implementations based on their - targeted conformance profile: + Core support and MUST be supported by implementations based on the objects + they support: - HTTP Profile + HTTPRoute 1. HTTPRoute, Port: 80, Protocol: HTTP 2. HTTPRoute, Port: 443, Protocol: HTTPS, TLS Mode: Terminate, TLS keypair provided - TLS Profile + TLSRoute 1. TLSRoute, Port: 443, Protocol: TLS, TLS Mode: Passthrough "Distinct" Listeners have the following property: - The implementation can match inbound requests to a single distinct - Listener. When multiple Listeners share values for fields (for + **The implementation can match inbound requests to a single distinct + Listener**. + + When multiple Listeners share values for fields (for example, two Listeners with the same Port value), the implementation can match requests to only one of the Listeners using other Listener fields. - For example, the following Listener scenarios are distinct: + When multiple listeners have the same value for the Protocol field, then + each of the Listeners with matching Protocol values MUST have different + values for other fields. - 1. Multiple Listeners with the same Port that all use the "HTTP" - Protocol that all have unique Hostname values. - 2. Multiple Listeners with the same Port that use either the "HTTPS" or - "TLS" Protocol that all have unique Hostname values. - 3. A mixture of "TCP" and "UDP" Protocol Listeners, where no Listener - with the same Protocol has the same Port value. + The set of fields that MUST be different for a Listener differs per protocol. + The following rules define the rules for what fields MUST be considered for + Listeners to be distinct with each protocol currently defined in the + Gateway API spec. - Some fields in the Listener struct have possible values that affect - whether the Listener is distinct. Hostname is particularly relevant - for HTTP or HTTPS protocols. + The set of listeners that all share a protocol value MUST have _different_ + values for _at least one_ of these fields to be distinct: - When using the Hostname value to select between same-Port, same-Protocol - Listeners, the Hostname value must be different on each Listener for the - Listener to be distinct. + * **HTTP, HTTPS, TLS**: Port, Hostname + * **TCP, UDP**: Port - When the Listeners are distinct based on Hostname, inbound request + One **very** important rule to call out involves what happens when an + implementation: + + * Supports TCP protocol Listeners, as well as HTTP, HTTPS, or TLS protocol + Listeners, and + * sees HTTP, HTTPS, or TLS protocols with the same `port` as one with TCP + Protocol. + + In this case all the Listeners that share a port with the + TCP Listener are not distinct and so MUST NOT be accepted. + + If an implementation does not support TCP Protocol Listeners, then the + previous rule does not apply, and the TCP Listeners SHOULD NOT be + accepted. + + Note that the `tls` field is not used for determining if a listener is distinct, because + Listeners that _only_ differ on TLS config will still conflict in all cases. + + ### Listeners that are distinct only by Hostname + + When the Listeners are distinct based only on Hostname, inbound request hostnames MUST match from the most specific to least specific Hostname values to choose the correct Listener and its associated set of Routes. - Exact matches must be processed before wildcard matches, and wildcard - matches must be processed before fallback (empty Hostname value) + Exact matches MUST be processed before wildcard matches, and wildcard + matches MUST be processed before fallback (empty Hostname value) matches. For example, `"foo.example.com"` takes precedence over `"*.example.com"`, and `"*.example.com"` takes precedence over `""`. Additionally, if there are multiple wildcard entries, more specific wildcard entries must be processed before less specific wildcard entries. For example, `"*.foo.example.com"` takes precedence over `"*.example.com"`. + The precise definition here is that the higher the number of dots in the hostname to the right of the wildcard character, the higher the precedence. @@ -3224,18 +4044,26 @@ spec: the left, however, so `"*.example.com"` will match both `"foo.bar.example.com"` _and_ `"bar.example.com"`. + ## Handling indistinct Listeners + If a set of Listeners contains Listeners that are not distinct, then those - Listeners are Conflicted, and the implementation MUST set the "Conflicted" + Listeners are _Conflicted_, and the implementation MUST set the "Conflicted" condition in the Listener Status to "True". + The words "indistinct" and "conflicted" are considered equivalent for the + purpose of this documentation. + Implementations MAY choose to accept a Gateway with some Conflicted Listeners only if they only accept the partial Listener set that contains - no Conflicted Listeners. To put this another way, implementations may - accept a partial Listener set only if they throw out *all* the conflicting - Listeners. No picking one of the conflicting listeners as the winner. - This also means that the Gateway must have at least one non-conflicting - Listener in this case, otherwise it violates the requirement that at - least one Listener must be present. + no Conflicted Listeners. + + Specifically, an implementation MAY accept a partial Listener set subject to + the following rules: + + * The implementation MUST NOT pick one conflicting Listener as the winner. + ALL indistinct Listeners must not be accepted for processing. + * At least one distinct Listener MUST be present, or else the Gateway effectively + contains _no_ Listeners, and must be rejected from processing as a whole. The implementation MUST set a "ListenersNotValid" condition on the Gateway Status when the Gateway contains Conflicted Listeners whether or @@ -3244,7 +4072,25 @@ spec: Accepted. Additionally, the Listener status for those listeners SHOULD indicate which Listeners are conflicted and not Accepted. - A Gateway's Listeners are considered "compatible" if: + ## General Listener behavior + + Note that, for all distinct Listeners, requests SHOULD match at most one Listener. + For example, if Listeners are defined for "foo.example.com" and "*.example.com", a + request to "foo.example.com" SHOULD only be routed using routes attached + to the "foo.example.com" Listener (and not the "*.example.com" Listener). + + This concept is known as "Listener Isolation", and it is an Extended feature + of Gateway API. Implementations that do not support Listener Isolation MUST + clearly document this, and MUST NOT claim support for the + `GatewayHTTPListenerIsolation` feature. + + Implementations that _do_ support Listener Isolation SHOULD claim support + for the Extended `GatewayHTTPListenerIsolation` feature and pass the associated + conformance tests. + + ## Compatible Listeners + + A Gateway's Listeners are considered _compatible_ if: 1. They are distinct. 2. The implementation can serve them in compliance with the Addresses @@ -3259,16 +4105,11 @@ spec: on the same address, or cannot mix HTTPS and generic TLS listens on the same port would not consider those cases compatible, even though they are distinct. - Note that requests SHOULD match at most one Listener. For example, if - Listeners are defined for "foo.example.com" and "*.example.com", a - request to "foo.example.com" SHOULD only be routed using routes attached - to the "foo.example.com" Listener (and not the "*.example.com" Listener). - This concept is known as "Listener Isolation". Implementations that do - not support Listener Isolation MUST clearly document this. - Implementations MAY merge separate Gateways onto a single set of Addresses if all Listeners across all Gateways are compatible. + In a future release the MinItems=1 requirement MAY be dropped. + Support: Core items: description: |- @@ -3339,6 +4180,7 @@ spec: type: object maxItems: 8 type: array + x-kubernetes-list-type: atomic namespaces: default: from: Same @@ -3430,10 +4272,31 @@ spec: * TLS: The Listener Hostname MUST match the SNI. * HTTP: The Listener Hostname MUST match the Host header of the request. - * HTTPS: The Listener Hostname SHOULD match at both the TLS and HTTP - protocol layers as described above. If an implementation does not - ensure that both the SNI and Host header match the Listener hostname, - it MUST clearly document that. + * HTTPS: The Listener Hostname SHOULD match both the SNI and Host header. + Note that this does not require the SNI and Host header to be the same. + The semantics of this are described in more detail below. + + To ensure security, Section 11.1 of RFC-6066 emphasizes that server + implementations that rely on SNI hostname matching MUST also verify + hostnames within the application protocol. + + Section 9.1.2 of RFC-7540 provides a mechanism for servers to reject the + reuse of a connection by responding with the HTTP 421 Misdirected Request + status code. This indicates that the origin server has rejected the + request because it appears to have been misdirected. + + To detect misdirected requests, Gateways SHOULD match the authority of + the requests with all the SNI hostname(s) configured across all the + Gateway Listeners on the same port and protocol: + + * If another Listener has an exact match or more specific wildcard entry, + the Gateway SHOULD return a 421. + * If the current Listener (selected by SNI matching during ClientHello) + does not match the Host: + * If another Listener does match the Host, the Gateway SHOULD return a + 421. + * If no other Listener matches the Host, the Gateway MUST return a + 404. For HTTPRoute and TLSRoute resources, there is an interaction with the `spec.hostnames` array. When both listener and route specify hostnames, @@ -3485,7 +4348,7 @@ spec: the Protocol field is "HTTPS" or "TLS". It is invalid to set this field if the Protocol field is "HTTP", "TCP", or "UDP". - The association of SNIs to Certificate defined in GatewayTLSConfig is + The association of SNIs to Certificate defined in ListenerTLSConfig is defined based on the Hostname field for this listener. The GatewayClass MUST use the longest matching SNI out of all @@ -3572,94 +4435,7 @@ spec: type: object maxItems: 64 type: array - frontendValidation: - description: |+ - FrontendValidation holds configuration information for validating the frontend (client). - Setting this field will require clients to send a client certificate - required for validation during the TLS handshake. In browsers this may result in a dialog appearing - that requests a user to specify the client certificate. - The maximum depth of a certificate chain accepted in verification is Implementation specific. - - Support: Extended - - properties: - caCertificateRefs: - description: |- - CACertificateRefs contains one or more references to - Kubernetes objects that contain TLS certificates of - the Certificate Authorities that can be used - as a trust anchor to validate the certificates presented by the client. - - A single CA certificate reference to a Kubernetes ConfigMap - has "Core" support. - Implementations MAY choose to support attaching multiple CA certificates to - a Listener, but this behavior is implementation-specific. - - Support: Core - A single reference to a Kubernetes ConfigMap - with the CA certificate in a key named `ca.crt`. - - Support: Implementation-specific (More than one reference, or other kinds - of resources). - - References to a resource in a different namespace are invalid UNLESS there - is a ReferenceGrant in the target namespace that allows the certificate - to be attached. If a ReferenceGrant does not allow this reference, the - "ResolvedRefs" condition MUST be set to False for this listener with the - "RefNotPermitted" reason. - items: - description: |- - ObjectReference identifies an API object including its namespace. - - The API object must be valid in the cluster; the Group and Kind must - be registered in the cluster for this reference to be valid. - - References to objects with invalid Group and Kind are not valid, and must - be rejected by the implementation, with appropriate Conditions set - on the containing object. - properties: - group: - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - description: Kind is kind of the referent. For - example "ConfigMap" or "Service". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - group - - kind - - name - type: object - maxItems: 8 - minItems: 1 - type: array - type: object + x-kubernetes-list-type: atomic mode: default: Terminate description: |- @@ -3728,6 +4504,9 @@ spec: - message: tls mode must be Terminate for protocol HTTPS rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' - message: hostname must not be specified for protocols ['TCP', 'UDP'] rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) || l.hostname == '''') : true)' @@ -3738,6 +4517,421 @@ spec: rule: 'self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname == l2.hostname : !has(l1.hostname) && !has(l2.hostname))))' + tls: + description: |- + TLS specifies frontend and backend tls configuration for entire gateway. + + Support: Extended + properties: + backend: + description: |- + Backend describes TLS configuration for gateway when connecting + to backends. + + Note that this contains only details for the Gateway as a TLS client, + and does _not_ imply behavior about how to choose which backend should + get a TLS connection. That is determined by the presence of a BackendTLSPolicy. + + Support: Core + properties: + clientCertificateRef: + description: |- + ClientCertificateRef references an object that contains a client certificate + and its associated private key. It can reference standard Kubernetes resources, + i.e., Secret, or implementation-specific custom resources. + + A ClientCertificateRef is considered invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a Secret does not contain the keys + named `tls.crt` and `tls.key`). In this case, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `InvalidClientCertificateRef` + and the Message of the Condition MUST indicate why the reference is invalid. + + * It refers to a resource in another namespace UNLESS there is a ReferenceGrant + in the target namespace that allows the certificate to be attached. + If a ReferenceGrant does not allow this reference, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message MUST be set. + + Support: Core - Reference to a Kubernetes TLS Secret (with the type `kubernetes.io/tls`). + Support: Implementation-specific - Other resource kinds or Secrets with a + different type (e.g., `Opaque`). + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + type: object + frontend: + description: |- + Frontend describes TLS config when client connects to Gateway. + Support: Core + properties: + default: + description: |- + Default specifies the default client certificate validation configuration + for all Listeners handling HTTPS traffic, unless a per-port configuration + is defined. + + support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + perPort: + description: |- + PerPort specifies tls configuration assigned per port. + Per port configuration is optional. Once set this configuration overrides + the default configuration for all Listeners handling HTTPS traffic + that match this port. + Each override port requires a unique TLS configuration. + + support: Core + items: + properties: + port: + description: |- + The Port indicates the Port Number to which the TLS configuration will be + applied. This configuration will be applied to all Listeners handling HTTPS + traffic that match this port. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + tls: + description: |- + TLS store the configuration that will be applied to all Listeners handling + HTTPS traffic and matching given port. + + Support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + For example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + required: + - port + - tls + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - port + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: Port for TLS configuration must be unique within + the Gateway + rule: self.all(t1, self.exists_one(t2, t1.port == t2.port)) + required: + - default + type: object + type: object required: - gatewayClassName - listeners @@ -3758,7 +4952,7 @@ spec: description: Status defines the current state of Gateway. properties: addresses: - description: |+ + description: |- Addresses lists the network addresses that have been bound to the Gateway. @@ -3768,7 +4962,6 @@ spec: * no addresses are specified, all addresses are dynamically assigned * a combination of specified and dynamic addresses are assigned * a specified address was unusable (e.g. already in use) - items: description: GatewayStatusAddress describes a network address that is bound to a Gateway. @@ -3813,6 +5006,21 @@ spec: true' maxItems: 16 type: array + x-kubernetes-list-type: atomic + attachedListenerSets: + description: |- + AttachedListenerSets represents the total number of ListenerSets that have been + successfully attached to this Gateway. + + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + + Uses for this field include troubleshooting AttachedListenerSets attachment and + measuring blast radius/impact of changes to a Gateway. + format: int32 + type: integer conditions: default: - lastTransitionTime: "1970-01-01T00:00:00Z" @@ -3917,8 +5125,11 @@ spec: attachment semantics can be found in the documentation on the various Route kinds ParentRefs fields). Listener or Route status does not impact successful attachment, i.e. the AttachedRoutes field count MUST be set - for Listeners with condition Accepted: false and MUST count successfully - attached Routes that may themselves have Accepted: false conditions. + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. Uses for this field include troubleshooting Route attachment and measuring blast radius/impact of changes to a Listener. @@ -3997,7 +5208,7 @@ spec: supportedKinds: description: |- SupportedKinds is the list indicating the Kinds supported by this - listener. This MUST represent the kinds an implementation supports for + listener. This MUST represent the kinds supported by an implementation for that Listener configuration. If kinds are specified in Spec that are not supported, they MUST NOT @@ -4026,11 +5237,11 @@ spec: type: object maxItems: 8 type: array + x-kubernetes-list-type: atomic required: - attachedRoutes - conditions - name - - supportedKinds type: object maxItems: 64 type: array @@ -4056,10 +5267,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: grpcroutes.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -4205,8 +5415,9 @@ spec: type: string maxItems: 16 type: array + x-kubernetes-list-type: atomic parentRefs: - description: |+ + description: |- ParentRefs references the resources (usually Gateways) that a Route wants to be attached to. Note that the referenced parent resource needs to allow this for the attachment to be complete. For Gateways, that means @@ -4268,11 +5479,6 @@ spec: connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. - - - - - items: description: |- ParentReference identifies an API object (usually a Gateway) that can be considered @@ -4422,6 +5628,7 @@ spec: type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: sectionName or port must be specified when parentRefs includes 2 or more references to the same parent @@ -4446,9 +5653,7 @@ spec: || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port == p2.port)))) rules: - description: |+ - Rules are a list of GRPC matchers, filters and actions. - + description: Rules are a list of GRPC matchers, filters and actions. items: description: |- GRPCRouteRule defines the semantics for matching a gRPC request based on @@ -4494,7 +5699,6 @@ spec: namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - When the BackendRef points to a Kubernetes Service, implementations SHOULD honor the appProtocol field if it is set for the target Service Port. @@ -4509,8 +5713,6 @@ spec: If a Route is not able to send traffic to the backend using the specified protocol then the backend is considered invalid. Implementations MUST set the "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. - - properties: filters: description: |- @@ -4596,7 +5798,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -4608,10 +5810,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -4671,7 +5877,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -4683,10 +5889,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -4699,7 +5909,7 @@ spec: x-kubernetes-list-type: map type: object requestMirror: - description: |+ + description: |- RequestMirror defines a schema for a filter that mirrors requests. Requests are sent to the specified destination, but responses from that destination are ignored. @@ -4709,7 +5919,6 @@ spec: backends. Support: Extended - properties: backendRef: description: |- @@ -4805,13 +6014,12 @@ spec: rule: '(size(self.group) == 0 && self.kind == ''Service'') ? has(self.port) : true' fraction: - description: |+ + description: |- Fraction represents the fraction of requests that should be mirrored to BackendRef. Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - properties: denominator: default: 100 @@ -4830,14 +6038,13 @@ spec: to denominator rule: self.numerator <= self.denominator percent: - description: |+ + description: |- Percent represents the percentage of requests that should be mirrored to BackendRef. Its minimum value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of requests). Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - format: int32 maximum: 100 minimum: 0 @@ -4882,7 +6089,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -4894,10 +6101,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -4957,7 +6168,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -4969,10 +6180,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -4985,7 +6200,7 @@ spec: x-kubernetes-list-type: map type: object type: - description: |+ + description: |- Type identifies the type of filter to apply. As with other API fields, types are classified into three conformance levels: @@ -5010,7 +6225,6 @@ spec: If a reference to a custom filter type cannot be resolved, the filter MUST NOT be skipped. Instead, requests that would have been processed by that filter MUST receive a HTTP error response. - enum: - ResponseHeaderModifier - RequestHeaderModifier @@ -5052,6 +6266,7 @@ spec: rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: RequestHeaderModifier filter cannot be repeated rule: self.filter(f, f.type == 'RequestHeaderModifier').size() @@ -5148,6 +6363,7 @@ spec: ? has(self.port) : true' maxItems: 16 type: array + x-kubernetes-list-type: atomic filters: description: |- Filters define the filters that are applied to requests that match @@ -5167,7 +6383,7 @@ spec: Specifying the same filter multiple times is not supported unless explicitly indicated in the filter. - If an implementation can not support a combination of filters, it must clearly + If an implementation cannot support a combination of filters, it must clearly document that limitation. In cases where incompatible or unsupported filters are specified and cause the `Accepted` condition to be set to status `False`, implementations may use the `IncompatibleFilters` reason to specify @@ -5250,7 +6466,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -5262,10 +6478,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -5324,7 +6544,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -5336,10 +6556,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -5352,7 +6576,7 @@ spec: x-kubernetes-list-type: map type: object requestMirror: - description: |+ + description: |- RequestMirror defines a schema for a filter that mirrors requests. Requests are sent to the specified destination, but responses from that destination are ignored. @@ -5362,7 +6586,6 @@ spec: backends. Support: Extended - properties: backendRef: description: |- @@ -5458,13 +6681,12 @@ spec: rule: '(size(self.group) == 0 && self.kind == ''Service'') ? has(self.port) : true' fraction: - description: |+ + description: |- Fraction represents the fraction of requests that should be mirrored to BackendRef. Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - properties: denominator: default: 100 @@ -5483,14 +6705,13 @@ spec: denominator rule: self.numerator <= self.denominator percent: - description: |+ + description: |- Percent represents the percentage of requests that should be mirrored to BackendRef. Its minimum value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of requests). Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - format: int32 maximum: 100 minimum: 0 @@ -5534,7 +6755,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -5546,10 +6767,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -5608,7 +6833,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -5620,10 +6845,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -5636,7 +6865,7 @@ spec: x-kubernetes-list-type: map type: object type: - description: |+ + description: |- Type identifies the type of filter to apply. As with other API fields, types are classified into three conformance levels: @@ -5661,7 +6890,6 @@ spec: If a reference to a custom filter type cannot be resolved, the filter MUST NOT be skipped. Instead, requests that would have been processed by that filter MUST receive a HTTP error response. - enum: - ResponseHeaderModifier - RequestHeaderModifier @@ -5702,6 +6930,7 @@ spec: rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: RequestHeaderModifier filter cannot be repeated rule: self.filter(f, f.type == 'RequestHeaderModifier').size() @@ -5774,8 +7003,8 @@ spec: - method: type: Exact service: "foo" - headers: - - name: "version" + - headers: + name: "version" value "v1" ``` @@ -5877,10 +7106,11 @@ spec: has(self.method) ? self.method.matches(r"""^[A-Za-z_][A-Za-z_0-9]*$"""): true' type: object - maxItems: 8 + maxItems: 64 type: array + x-kubernetes-list-type: atomic name: - description: | + description: |- Name is the name of the route rule. This name MUST be unique within a Route if it is set. Support: Extended @@ -5889,12 +7119,11 @@ spec: pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string sessionPersistence: - description: |+ + description: |- SessionPersistence defines and configures session persistence for the route rule. Support: Extended - properties: absoluteTimeout: description: |- @@ -5929,6 +7158,8 @@ spec: absolute lifetime of the cookie tracked by the gateway and is optional. + Defaults to "Session". + Support: Core for "Session" type Support: Extended for "Permanent" type @@ -5960,7 +7191,7 @@ spec: default: Cookie description: |- Type defines the type of session persistence such as through - the use a header or cookie. Defaults to cookie based session + the use of a header or cookie. Defaults to cookie based session persistence. Support: Core for "Cookie" type @@ -5976,9 +7207,12 @@ spec: is Permanent rule: '!has(self.cookieConfig) || !has(self.cookieConfig.lifetimeType) || self.cookieConfig.lifetimeType != ''Permanent'' || has(self.absoluteTimeout)' + - message: cookieConfig can only be set with type Cookie + rule: '!has(self.cookieConfig) || self.type == ''Cookie''' type: object maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: While 16 rules and 64 matches per rule are allowed, the total number of matches across all rules in a route must be less @@ -6003,6 +7237,24 @@ spec: - message: Rule name must be unique within the route rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name)) + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string type: object status: description: Status defines the current state of GRPCRoute. @@ -6046,9 +7298,9 @@ spec: There are a number of cases where the "Accepted" condition may not be set due to lack of controller visibility, that includes when: - * The Route refers to a non-existent parent. + * The Route refers to a nonexistent parent. * The Route is of a type that the controller does not support. - * The Route is in a namespace the controller does not have access to. + * The Route is in a namespace to which the controller does not have access. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -6267,14 +7519,18 @@ spec: - name type: object required: + - conditions - controllerName - parentRef type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic required: - parents type: object + required: + - spec type: object served: true storage: true @@ -6291,10 +7547,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: httproutes.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -6420,8 +7675,9 @@ spec: type: string maxItems: 16 type: array + x-kubernetes-list-type: atomic parentRefs: - description: |+ + description: |- ParentRefs references the resources (usually Gateways) that a Route wants to be attached to. Note that the referenced parent resource needs to allow this for the attachment to be complete. For Gateways, that means @@ -6483,11 +7739,6 @@ spec: connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. - - - - - items: description: |- ParentReference identifies an API object (usually a Gateway) that can be considered @@ -6637,6 +7888,7 @@ spec: type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: sectionName or port must be specified when parentRefs includes 2 or more references to the same parent @@ -6666,9 +7918,7 @@ spec: - path: type: PathPrefix value: / - description: |+ - Rules are a list of HTTP matchers, filters and actions. - + description: Rules are a list of HTTP matchers, filters and actions. items: description: |- HTTPRouteRule defines semantics for matching an HTTP request based on @@ -6721,7 +7971,6 @@ spec: namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - When the BackendRef points to a Kubernetes Service, implementations SHOULD honor the appProtocol field if it is set for the target Service Port. @@ -6736,8 +7985,6 @@ spec: If a Route is not able to send traffic to the backend using the specified protocol then the backend is considered invalid. Implementations MUST set the "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. - - properties: filters: description: |- @@ -6755,6 +8002,316 @@ spec: authentication strategies, rate-limiting, and traffic shaping. API guarantee/conformance is defined based on the type of the filter. properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object extensionRef: description: |- ExtensionRef is an optional, implementation-specific extension to the @@ -6790,6 +8347,256 @@ spec: - kind - name type: object + externalAuth: + description: |- + ExternalAuth configures settings related to sending request details + to an external auth service. The external service MUST authenticate + the request, and MAY authorize the request as well. + + If there is any problem communicating with the external service, + this filter MUST fail closed. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef is a reference to a backend to send authorization + requests to. + + The backend must speak the selected protocol (GRPC or HTTP) on the + referenced port. + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + forwardBody: + description: |- + ForwardBody controls if requests to the authorization server should include + the body of the client request; and if so, how big that body is allowed + to be. + + It is expected that implementations will buffer the request body up to + `forwardBody.maxSize` bytes. Bodies over that size must be rejected with a + 4xx series error (413 or 403 are common examples), and fail processing + of the filter. + + If unset, or `forwardBody.maxSize` is set to `0`, then the body will not + be forwarded. + + Feature Name: HTTPRouteExternalAuthForwardBody + properties: + maxSize: + description: |- + MaxSize specifies how large in bytes the largest body that will be buffered + and sent to the authorization server. If the body size is larger than + `maxSize`, then the body sent to the authorization server must be + truncated to `maxSize` bytes. + + Experimental note: This behavior needs to be checked against + various dataplanes; it may need to be changed. + See https://github.com/kubernetes-sigs/gateway-api/pull/4001#discussion_r2291405746 + for more. + + If 0, the body will not be sent to the authorization server. + type: integer + type: object + grpc: + description: |- + GRPCAuthConfig contains configuration for communication with ext_authz + protocol-speaking backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what headers from the client request + will be sent to the authorization server. + + If this list is empty, then all headers must be sent. + + If the list has entries, only those entries must be sent. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + type: object + http: + description: |- + HTTPAuthConfig contains configuration for communication with HTTP-speaking + backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what additional headers from the client request + will be sent to the authorization server. + + The following headers must always be sent to the authorization server, + regardless of this setting: + + * `Host` + * `Method` + * `Path` + * `Content-Length` + * `Authorization` + + If this list is empty, then only those headers must be sent. + + Note that `Content-Length` has a special behavior, in that the length + sent must be correct for the actual request to the external authorization + server - that is, it must reflect the actual number of bytes sent in the + body of the request to the authorization server. + + So if the `forwardBody` stanza is unset, or `forwardBody.maxSize` is set + to `0`, then `Content-Length` must be `0`. If `forwardBody.maxSize` is set + to anything other than `0`, then the `Content-Length` of the authorization + request must be set to the actual number of bytes forwarded. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + allowedResponseHeaders: + description: |- + AllowedResponseHeaders specifies what headers from the authorization response + will be copied into the request to the backend. + + If this list is empty, then all headers from the authorization server + except Authority or Host must be copied. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + path: + description: |- + Path sets the prefix that paths from the client request will have added + when forwarded to the authorization server. + + When empty or unspecified, no prefix is added. + + Valid values are the same as the "value" regex for path values in the `match` + stanza, and the validation regex will screen out invalid paths in the same way. + Even with the validation, implementations MUST sanitize this input before using it + directly. + maxLength: 1024 + pattern: ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$ + type: string + type: object + protocol: + description: |- + ExternalAuthProtocol describes which protocol to use when communicating with an + ext_authz authorization server. + + When this is set to GRPC, each backend must use the Envoy ext_authz protocol + on the port specified in `backendRefs`. Requests and responses are defined + in the protobufs explained at: + https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + + When this is set to HTTP, each backend must respond with a `200` status + code in on a successful authorization. Any other code is considered + an authorization failure. + + Feature Names: + GRPC Support - HTTPRouteExternalAuthGRPC + HTTP Support - HTTPRouteExternalAuthHTTP + enum: + - HTTP + - GRPC + type: string + required: + - backendRef + - protocol + type: object + x-kubernetes-validations: + - message: grpc must be specified when protocol + is set to 'GRPC' + rule: 'self.protocol == ''GRPC'' ? has(self.grpc) + : true' + - message: protocol must be 'GRPC' when grpc is + set + rule: 'has(self.grpc) ? self.protocol == ''GRPC'' + : true' + - message: http must be specified when protocol + is set to 'HTTP' + rule: 'self.protocol == ''HTTP'' ? has(self.http) + : true' + - message: protocol must be 'HTTP' when http is + set + rule: 'has(self.http) ? self.protocol == ''HTTP'' + : true' requestHeaderModifier: description: |- RequestHeaderModifier defines a schema for a filter that modifies request @@ -6823,7 +8630,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -6835,10 +8642,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -6898,7 +8709,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -6910,10 +8721,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -6926,7 +8741,7 @@ spec: x-kubernetes-list-type: map type: object requestMirror: - description: |+ + description: |- RequestMirror defines a schema for a filter that mirrors requests. Requests are sent to the specified destination, but responses from that destination are ignored. @@ -6936,7 +8751,6 @@ spec: backends. Support: Extended - properties: backendRef: description: |- @@ -7032,13 +8846,12 @@ spec: rule: '(size(self.group) == 0 && self.kind == ''Service'') ? has(self.port) : true' fraction: - description: |+ + description: |- Fraction represents the fraction of requests that should be mirrored to BackendRef. Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - properties: denominator: default: 100 @@ -7057,14 +8870,13 @@ spec: to denominator rule: self.numerator <= self.denominator percent: - description: |+ + description: |- Percent represents the percentage of requests that should be mirrored to BackendRef. Its minimum value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of requests). Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - format: int32 maximum: 100 minimum: 0 @@ -7227,6 +9039,9 @@ spec: enum: - 301 - 302 + - 303 + - 307 + - 308 type: integer type: object responseHeaderModifier: @@ -7262,7 +9077,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -7274,10 +9089,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -7337,7 +9156,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -7349,10 +9168,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -7405,6 +9228,8 @@ spec: - RequestRedirect - URLRewrite - ExtensionRef + - CORS + - ExternalAuth type: string urlRewrite: description: |- @@ -7494,6 +9319,11 @@ spec: - type type: object x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type + is not CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' - message: filter.requestHeaderModifier must be nil if the filter.type is not RequestHeaderModifier rule: '!(has(self.requestHeaderModifier) && self.type @@ -7537,17 +9367,22 @@ spec: - message: filter.extensionRef must be specified for ExtensionRef filter.type rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + - message: filter.externalAuth must be nil if the filter.type + is not ExternalAuth + rule: '!(has(self.externalAuth) && self.type != ''ExternalAuth'')' + - message: filter.externalAuth must be specified for + ExternalAuth filter.type + rule: '!(!has(self.externalAuth) && self.type == ''ExternalAuth'')' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both rule: '!(self.exists(f, f.type == ''RequestRedirect'') && self.exists(f, f.type == ''URLRewrite''))' - - message: May specify either httpRouteFilterRequestRedirect - or httpRouteFilterRequestRewrite, but not both - rule: '!(self.exists(f, f.type == ''RequestRedirect'') - && self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 - message: RequestHeaderModifier filter cannot be repeated rule: self.filter(f, f.type == 'RequestHeaderModifier').size() <= 1 @@ -7649,6 +9484,7 @@ spec: ? has(self.port) : true' maxItems: 16 type: array + x-kubernetes-list-type: atomic filters: description: |- Filters define the filters that are applied to requests that match @@ -7658,7 +9494,7 @@ spec: they are specified. Implementations MAY choose to implement this ordering strictly, rejecting - any combination or order of filters that can not be supported. If implementations + any combination or order of filters that cannot be supported. If implementations choose a strict interpretation of filter ordering, they MUST clearly document that behavior. @@ -7680,7 +9516,7 @@ spec: All filters are expected to be compatible with each other except for the URLRewrite and RequestRedirect filters, which may not be combined. If an - implementation can not support other combinations of filters, they must clearly + implementation cannot support other combinations of filters, they must clearly document that limitation. In cases where incompatible or unsupported filters are specified and cause the `Accepted` condition to be set to status `False`, implementations may use the `IncompatibleFilters` reason to specify @@ -7696,6 +9532,316 @@ spec: authentication strategies, rate-limiting, and traffic shaping. API guarantee/conformance is defined based on the type of the filter. properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object extensionRef: description: |- ExtensionRef is an optional, implementation-specific extension to the @@ -7731,6 +9877,254 @@ spec: - kind - name type: object + externalAuth: + description: |- + ExternalAuth configures settings related to sending request details + to an external auth service. The external service MUST authenticate + the request, and MAY authorize the request as well. + + If there is any problem communicating with the external service, + this filter MUST fail closed. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef is a reference to a backend to send authorization + requests to. + + The backend must speak the selected protocol (GRPC or HTTP) on the + referenced port. + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + forwardBody: + description: |- + ForwardBody controls if requests to the authorization server should include + the body of the client request; and if so, how big that body is allowed + to be. + + It is expected that implementations will buffer the request body up to + `forwardBody.maxSize` bytes. Bodies over that size must be rejected with a + 4xx series error (413 or 403 are common examples), and fail processing + of the filter. + + If unset, or `forwardBody.maxSize` is set to `0`, then the body will not + be forwarded. + + Feature Name: HTTPRouteExternalAuthForwardBody + properties: + maxSize: + description: |- + MaxSize specifies how large in bytes the largest body that will be buffered + and sent to the authorization server. If the body size is larger than + `maxSize`, then the body sent to the authorization server must be + truncated to `maxSize` bytes. + + Experimental note: This behavior needs to be checked against + various dataplanes; it may need to be changed. + See https://github.com/kubernetes-sigs/gateway-api/pull/4001#discussion_r2291405746 + for more. + + If 0, the body will not be sent to the authorization server. + type: integer + type: object + grpc: + description: |- + GRPCAuthConfig contains configuration for communication with ext_authz + protocol-speaking backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what headers from the client request + will be sent to the authorization server. + + If this list is empty, then all headers must be sent. + + If the list has entries, only those entries must be sent. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + type: object + http: + description: |- + HTTPAuthConfig contains configuration for communication with HTTP-speaking + backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what additional headers from the client request + will be sent to the authorization server. + + The following headers must always be sent to the authorization server, + regardless of this setting: + + * `Host` + * `Method` + * `Path` + * `Content-Length` + * `Authorization` + + If this list is empty, then only those headers must be sent. + + Note that `Content-Length` has a special behavior, in that the length + sent must be correct for the actual request to the external authorization + server - that is, it must reflect the actual number of bytes sent in the + body of the request to the authorization server. + + So if the `forwardBody` stanza is unset, or `forwardBody.maxSize` is set + to `0`, then `Content-Length` must be `0`. If `forwardBody.maxSize` is set + to anything other than `0`, then the `Content-Length` of the authorization + request must be set to the actual number of bytes forwarded. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + allowedResponseHeaders: + description: |- + AllowedResponseHeaders specifies what headers from the authorization response + will be copied into the request to the backend. + + If this list is empty, then all headers from the authorization server + except Authority or Host must be copied. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + path: + description: |- + Path sets the prefix that paths from the client request will have added + when forwarded to the authorization server. + + When empty or unspecified, no prefix is added. + + Valid values are the same as the "value" regex for path values in the `match` + stanza, and the validation regex will screen out invalid paths in the same way. + Even with the validation, implementations MUST sanitize this input before using it + directly. + maxLength: 1024 + pattern: ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$ + type: string + type: object + protocol: + description: |- + ExternalAuthProtocol describes which protocol to use when communicating with an + ext_authz authorization server. + + When this is set to GRPC, each backend must use the Envoy ext_authz protocol + on the port specified in `backendRefs`. Requests and responses are defined + in the protobufs explained at: + https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + + When this is set to HTTP, each backend must respond with a `200` status + code in on a successful authorization. Any other code is considered + an authorization failure. + + Feature Names: + GRPC Support - HTTPRouteExternalAuthGRPC + HTTP Support - HTTPRouteExternalAuthHTTP + enum: + - HTTP + - GRPC + type: string + required: + - backendRef + - protocol + type: object + x-kubernetes-validations: + - message: grpc must be specified when protocol is set + to 'GRPC' + rule: 'self.protocol == ''GRPC'' ? has(self.grpc) : + true' + - message: protocol must be 'GRPC' when grpc is set + rule: 'has(self.grpc) ? self.protocol == ''GRPC'' : + true' + - message: http must be specified when protocol is set + to 'HTTP' + rule: 'self.protocol == ''HTTP'' ? has(self.http) : + true' + - message: protocol must be 'HTTP' when http is set + rule: 'has(self.http) ? self.protocol == ''HTTP'' : + true' requestHeaderModifier: description: |- RequestHeaderModifier defines a schema for a filter that modifies request @@ -7763,7 +10157,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -7775,10 +10169,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -7837,7 +10235,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -7849,10 +10247,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -7865,7 +10267,7 @@ spec: x-kubernetes-list-type: map type: object requestMirror: - description: |+ + description: |- RequestMirror defines a schema for a filter that mirrors requests. Requests are sent to the specified destination, but responses from that destination are ignored. @@ -7875,7 +10277,6 @@ spec: backends. Support: Extended - properties: backendRef: description: |- @@ -7971,13 +10372,12 @@ spec: rule: '(size(self.group) == 0 && self.kind == ''Service'') ? has(self.port) : true' fraction: - description: |+ + description: |- Fraction represents the fraction of requests that should be mirrored to BackendRef. Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - properties: denominator: default: 100 @@ -7996,14 +10396,13 @@ spec: denominator rule: self.numerator <= self.denominator percent: - description: |+ + description: |- Percent represents the percentage of requests that should be mirrored to BackendRef. Its minimum value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of requests). Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - format: int32 maximum: 100 minimum: 0 @@ -8166,6 +10565,9 @@ spec: enum: - 301 - 302 + - 303 + - 307 + - 308 type: integer type: object responseHeaderModifier: @@ -8200,7 +10602,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -8212,10 +10614,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -8274,7 +10680,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -8286,10 +10692,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -8342,6 +10752,8 @@ spec: - RequestRedirect - URLRewrite - ExtensionRef + - CORS + - ExternalAuth type: string urlRewrite: description: |- @@ -8431,6 +10843,11 @@ spec: - type type: object x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type is not + CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' - message: filter.requestHeaderModifier must be nil if the filter.type is not RequestHeaderModifier rule: '!(has(self.requestHeaderModifier) && self.type != @@ -8471,13 +10888,22 @@ spec: - message: filter.extensionRef must be specified for ExtensionRef filter.type rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + - message: filter.externalAuth must be nil if the filter.type + is not ExternalAuth + rule: '!(has(self.externalAuth) && self.type != ''ExternalAuth'')' + - message: filter.externalAuth must be specified for ExternalAuth + filter.type + rule: '!(!has(self.externalAuth) && self.type == ''ExternalAuth'')' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both rule: '!(self.exists(f, f.type == ''RequestRedirect'') && self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 - message: RequestHeaderModifier filter cannot be repeated rule: self.filter(f, f.type == 'RequestHeaderModifier').size() <= 1 @@ -8574,7 +11000,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, only the first entry with an equivalent name MUST be considered for a match. Subsequent @@ -8609,10 +11035,14 @@ spec: - RegularExpression type: string value: - description: Value is the value of HTTP Header to - be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -8784,8 +11214,9 @@ spec: type: object maxItems: 64 type: array + x-kubernetes-list-type: atomic name: - description: | + description: |- Name is the name of the route rule. This name MUST be unique within a Route if it is set. Support: Extended @@ -8794,15 +11225,14 @@ spec: pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string retry: - description: |+ + description: |- Retry defines the configuration for when to retry an HTTP request. Support: Extended - properties: attempts: description: |- - Attempts specifies the maxmimum number of times an individual request + Attempts specifies the maximum number of times an individual request from the gateway to a backend should be retried. If the maximum number of retries has been attempted without a successful @@ -8821,7 +11251,7 @@ spec: For example, setting the `rules[].retry.backoff` field to the value `100ms` will cause a backend request to first be retried approximately 100 milliseconds after timing out or receiving a response code configured - to be retryable. + to be retriable. An implementation MAY use an exponential or alternative backoff strategy for subsequent retry attempts, MAY cap the maximum backoff duration to @@ -8864,7 +11294,7 @@ spec: HTTPRouteRetryStatusCode defines an HTTP response status code for which a backend request should be retried. - Implementations MUST support the following status codes as retryable: + Implementations MUST support the following status codes as retriable: * 500 * 502 @@ -8876,20 +11306,18 @@ spec: Implementations MAY support specifying discrete values in the 400-499 range, which are often inadvisable to retry. - - maximum: 599 minimum: 400 type: integer type: array + x-kubernetes-list-type: atomic type: object sessionPersistence: - description: |+ + description: |- SessionPersistence defines and configures session persistence for the route rule. Support: Extended - properties: absoluteTimeout: description: |- @@ -8924,6 +11352,8 @@ spec: absolute lifetime of the cookie tracked by the gateway and is optional. + Defaults to "Session". + Support: Core for "Session" type Support: Extended for "Permanent" type @@ -8955,7 +11385,7 @@ spec: default: Cookie description: |- Type defines the type of session persistence such as through - the use a header or cookie. Defaults to cookie based session + the use of a header or cookie. Defaults to cookie based session persistence. Support: Core for "Cookie" type @@ -8971,6 +11401,8 @@ spec: is Permanent rule: '!has(self.cookieConfig) || !has(self.cookieConfig.lifetimeType) || self.cookieConfig.lifetimeType != ''Permanent'' || has(self.absoluteTimeout)' + - message: cookieConfig can only be set with type Cookie + rule: '!has(self.cookieConfig) || self.type == ''Cookie''' timeouts: description: |- Timeouts defines the timeouts that can be configured for an HTTP request. @@ -9075,7 +11507,9 @@ spec: != 1 || !has(self.matches[0].path) || self.matches[0].path.type != ''PathPrefix'') ? false : true) : true' maxItems: 16 + minItems: 1 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: While 16 rules and 64 matches per rule are allowed, the total number of matches across all rules in a route must be less @@ -9094,6 +11528,24 @@ spec: - message: Rule name must be unique within the route rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name)) + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string type: object status: description: Status defines the current state of HTTPRoute. @@ -9137,9 +11589,9 @@ spec: There are a number of cases where the "Accepted" condition may not be set due to lack of controller visibility, that includes when: - * The Route refers to a non-existent parent. + * The Route refers to a nonexistent parent. * The Route is of a type that the controller does not support. - * The Route is in a namespace the controller does not have access to. + * The Route is in a namespace to which the controller does not have access. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -9358,11 +11810,13 @@ spec: - name type: object required: + - conditions - controllerName - parentRef type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic required: - parents type: object @@ -9486,8 +11940,9 @@ spec: type: string maxItems: 16 type: array + x-kubernetes-list-type: atomic parentRefs: - description: |+ + description: |- ParentRefs references the resources (usually Gateways) that a Route wants to be attached to. Note that the referenced parent resource needs to allow this for the attachment to be complete. For Gateways, that means @@ -9549,11 +12004,6 @@ spec: connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. - - - - - items: description: |- ParentReference identifies an API object (usually a Gateway) that can be considered @@ -9703,6 +12153,7 @@ spec: type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: sectionName or port must be specified when parentRefs includes 2 or more references to the same parent @@ -9732,9 +12183,7 @@ spec: - path: type: PathPrefix value: / - description: |+ - Rules are a list of HTTP matchers, filters and actions. - + description: Rules are a list of HTTP matchers, filters and actions. items: description: |- HTTPRouteRule defines semantics for matching an HTTP request based on @@ -9787,7 +12236,6 @@ spec: namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - When the BackendRef points to a Kubernetes Service, implementations SHOULD honor the appProtocol field if it is set for the target Service Port. @@ -9802,8 +12250,6 @@ spec: If a Route is not able to send traffic to the backend using the specified protocol then the backend is considered invalid. Implementations MUST set the "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. - - properties: filters: description: |- @@ -9821,6 +12267,316 @@ spec: authentication strategies, rate-limiting, and traffic shaping. API guarantee/conformance is defined based on the type of the filter. properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object extensionRef: description: |- ExtensionRef is an optional, implementation-specific extension to the @@ -9856,6 +12612,256 @@ spec: - kind - name type: object + externalAuth: + description: |- + ExternalAuth configures settings related to sending request details + to an external auth service. The external service MUST authenticate + the request, and MAY authorize the request as well. + + If there is any problem communicating with the external service, + this filter MUST fail closed. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef is a reference to a backend to send authorization + requests to. + + The backend must speak the selected protocol (GRPC or HTTP) on the + referenced port. + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + forwardBody: + description: |- + ForwardBody controls if requests to the authorization server should include + the body of the client request; and if so, how big that body is allowed + to be. + + It is expected that implementations will buffer the request body up to + `forwardBody.maxSize` bytes. Bodies over that size must be rejected with a + 4xx series error (413 or 403 are common examples), and fail processing + of the filter. + + If unset, or `forwardBody.maxSize` is set to `0`, then the body will not + be forwarded. + + Feature Name: HTTPRouteExternalAuthForwardBody + properties: + maxSize: + description: |- + MaxSize specifies how large in bytes the largest body that will be buffered + and sent to the authorization server. If the body size is larger than + `maxSize`, then the body sent to the authorization server must be + truncated to `maxSize` bytes. + + Experimental note: This behavior needs to be checked against + various dataplanes; it may need to be changed. + See https://github.com/kubernetes-sigs/gateway-api/pull/4001#discussion_r2291405746 + for more. + + If 0, the body will not be sent to the authorization server. + type: integer + type: object + grpc: + description: |- + GRPCAuthConfig contains configuration for communication with ext_authz + protocol-speaking backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what headers from the client request + will be sent to the authorization server. + + If this list is empty, then all headers must be sent. + + If the list has entries, only those entries must be sent. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + type: object + http: + description: |- + HTTPAuthConfig contains configuration for communication with HTTP-speaking + backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what additional headers from the client request + will be sent to the authorization server. + + The following headers must always be sent to the authorization server, + regardless of this setting: + + * `Host` + * `Method` + * `Path` + * `Content-Length` + * `Authorization` + + If this list is empty, then only those headers must be sent. + + Note that `Content-Length` has a special behavior, in that the length + sent must be correct for the actual request to the external authorization + server - that is, it must reflect the actual number of bytes sent in the + body of the request to the authorization server. + + So if the `forwardBody` stanza is unset, or `forwardBody.maxSize` is set + to `0`, then `Content-Length` must be `0`. If `forwardBody.maxSize` is set + to anything other than `0`, then the `Content-Length` of the authorization + request must be set to the actual number of bytes forwarded. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + allowedResponseHeaders: + description: |- + AllowedResponseHeaders specifies what headers from the authorization response + will be copied into the request to the backend. + + If this list is empty, then all headers from the authorization server + except Authority or Host must be copied. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + path: + description: |- + Path sets the prefix that paths from the client request will have added + when forwarded to the authorization server. + + When empty or unspecified, no prefix is added. + + Valid values are the same as the "value" regex for path values in the `match` + stanza, and the validation regex will screen out invalid paths in the same way. + Even with the validation, implementations MUST sanitize this input before using it + directly. + maxLength: 1024 + pattern: ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$ + type: string + type: object + protocol: + description: |- + ExternalAuthProtocol describes which protocol to use when communicating with an + ext_authz authorization server. + + When this is set to GRPC, each backend must use the Envoy ext_authz protocol + on the port specified in `backendRefs`. Requests and responses are defined + in the protobufs explained at: + https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + + When this is set to HTTP, each backend must respond with a `200` status + code in on a successful authorization. Any other code is considered + an authorization failure. + + Feature Names: + GRPC Support - HTTPRouteExternalAuthGRPC + HTTP Support - HTTPRouteExternalAuthHTTP + enum: + - HTTP + - GRPC + type: string + required: + - backendRef + - protocol + type: object + x-kubernetes-validations: + - message: grpc must be specified when protocol + is set to 'GRPC' + rule: 'self.protocol == ''GRPC'' ? has(self.grpc) + : true' + - message: protocol must be 'GRPC' when grpc is + set + rule: 'has(self.grpc) ? self.protocol == ''GRPC'' + : true' + - message: http must be specified when protocol + is set to 'HTTP' + rule: 'self.protocol == ''HTTP'' ? has(self.http) + : true' + - message: protocol must be 'HTTP' when http is + set + rule: 'has(self.http) ? self.protocol == ''HTTP'' + : true' requestHeaderModifier: description: |- RequestHeaderModifier defines a schema for a filter that modifies request @@ -9889,7 +12895,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -9901,10 +12907,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -9964,7 +12974,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -9976,10 +12986,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -9992,7 +13006,7 @@ spec: x-kubernetes-list-type: map type: object requestMirror: - description: |+ + description: |- RequestMirror defines a schema for a filter that mirrors requests. Requests are sent to the specified destination, but responses from that destination are ignored. @@ -10002,7 +13016,6 @@ spec: backends. Support: Extended - properties: backendRef: description: |- @@ -10098,13 +13111,12 @@ spec: rule: '(size(self.group) == 0 && self.kind == ''Service'') ? has(self.port) : true' fraction: - description: |+ + description: |- Fraction represents the fraction of requests that should be mirrored to BackendRef. Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - properties: denominator: default: 100 @@ -10123,14 +13135,13 @@ spec: to denominator rule: self.numerator <= self.denominator percent: - description: |+ + description: |- Percent represents the percentage of requests that should be mirrored to BackendRef. Its minimum value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of requests). Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - format: int32 maximum: 100 minimum: 0 @@ -10293,6 +13304,9 @@ spec: enum: - 301 - 302 + - 303 + - 307 + - 308 type: integer type: object responseHeaderModifier: @@ -10328,7 +13342,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -10340,10 +13354,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -10403,7 +13421,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -10415,10 +13433,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP - Header to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -10471,6 +13493,8 @@ spec: - RequestRedirect - URLRewrite - ExtensionRef + - CORS + - ExternalAuth type: string urlRewrite: description: |- @@ -10560,6 +13584,11 @@ spec: - type type: object x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type + is not CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' - message: filter.requestHeaderModifier must be nil if the filter.type is not RequestHeaderModifier rule: '!(has(self.requestHeaderModifier) && self.type @@ -10603,17 +13632,22 @@ spec: - message: filter.extensionRef must be specified for ExtensionRef filter.type rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + - message: filter.externalAuth must be nil if the filter.type + is not ExternalAuth + rule: '!(has(self.externalAuth) && self.type != ''ExternalAuth'')' + - message: filter.externalAuth must be specified for + ExternalAuth filter.type + rule: '!(!has(self.externalAuth) && self.type == ''ExternalAuth'')' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both rule: '!(self.exists(f, f.type == ''RequestRedirect'') && self.exists(f, f.type == ''URLRewrite''))' - - message: May specify either httpRouteFilterRequestRedirect - or httpRouteFilterRequestRewrite, but not both - rule: '!(self.exists(f, f.type == ''RequestRedirect'') - && self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 - message: RequestHeaderModifier filter cannot be repeated rule: self.filter(f, f.type == 'RequestHeaderModifier').size() <= 1 @@ -10715,6 +13749,7 @@ spec: ? has(self.port) : true' maxItems: 16 type: array + x-kubernetes-list-type: atomic filters: description: |- Filters define the filters that are applied to requests that match @@ -10724,7 +13759,7 @@ spec: they are specified. Implementations MAY choose to implement this ordering strictly, rejecting - any combination or order of filters that can not be supported. If implementations + any combination or order of filters that cannot be supported. If implementations choose a strict interpretation of filter ordering, they MUST clearly document that behavior. @@ -10746,7 +13781,7 @@ spec: All filters are expected to be compatible with each other except for the URLRewrite and RequestRedirect filters, which may not be combined. If an - implementation can not support other combinations of filters, they must clearly + implementation cannot support other combinations of filters, they must clearly document that limitation. In cases where incompatible or unsupported filters are specified and cause the `Accepted` condition to be set to status `False`, implementations may use the `IncompatibleFilters` reason to specify @@ -10762,6 +13797,316 @@ spec: authentication strategies, rate-limiting, and traffic shaping. API guarantee/conformance is defined based on the type of the filter. properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object extensionRef: description: |- ExtensionRef is an optional, implementation-specific extension to the @@ -10797,6 +14142,254 @@ spec: - kind - name type: object + externalAuth: + description: |- + ExternalAuth configures settings related to sending request details + to an external auth service. The external service MUST authenticate + the request, and MAY authorize the request as well. + + If there is any problem communicating with the external service, + this filter MUST fail closed. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef is a reference to a backend to send authorization + requests to. + + The backend must speak the selected protocol (GRPC or HTTP) on the + referenced port. + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + forwardBody: + description: |- + ForwardBody controls if requests to the authorization server should include + the body of the client request; and if so, how big that body is allowed + to be. + + It is expected that implementations will buffer the request body up to + `forwardBody.maxSize` bytes. Bodies over that size must be rejected with a + 4xx series error (413 or 403 are common examples), and fail processing + of the filter. + + If unset, or `forwardBody.maxSize` is set to `0`, then the body will not + be forwarded. + + Feature Name: HTTPRouteExternalAuthForwardBody + properties: + maxSize: + description: |- + MaxSize specifies how large in bytes the largest body that will be buffered + and sent to the authorization server. If the body size is larger than + `maxSize`, then the body sent to the authorization server must be + truncated to `maxSize` bytes. + + Experimental note: This behavior needs to be checked against + various dataplanes; it may need to be changed. + See https://github.com/kubernetes-sigs/gateway-api/pull/4001#discussion_r2291405746 + for more. + + If 0, the body will not be sent to the authorization server. + type: integer + type: object + grpc: + description: |- + GRPCAuthConfig contains configuration for communication with ext_authz + protocol-speaking backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what headers from the client request + will be sent to the authorization server. + + If this list is empty, then all headers must be sent. + + If the list has entries, only those entries must be sent. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + type: object + http: + description: |- + HTTPAuthConfig contains configuration for communication with HTTP-speaking + backends. + + If unset, implementations must assume the default behavior for each + included field is intended. + properties: + allowedHeaders: + description: |- + AllowedRequestHeaders specifies what additional headers from the client request + will be sent to the authorization server. + + The following headers must always be sent to the authorization server, + regardless of this setting: + + * `Host` + * `Method` + * `Path` + * `Content-Length` + * `Authorization` + + If this list is empty, then only those headers must be sent. + + Note that `Content-Length` has a special behavior, in that the length + sent must be correct for the actual request to the external authorization + server - that is, it must reflect the actual number of bytes sent in the + body of the request to the authorization server. + + So if the `forwardBody` stanza is unset, or `forwardBody.maxSize` is set + to `0`, then `Content-Length` must be `0`. If `forwardBody.maxSize` is set + to anything other than `0`, then the `Content-Length` of the authorization + request must be set to the actual number of bytes forwarded. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + allowedResponseHeaders: + description: |- + AllowedResponseHeaders specifies what headers from the authorization response + will be copied into the request to the backend. + + If this list is empty, then all headers from the authorization server + except Authority or Host must be copied. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + path: + description: |- + Path sets the prefix that paths from the client request will have added + when forwarded to the authorization server. + + When empty or unspecified, no prefix is added. + + Valid values are the same as the "value" regex for path values in the `match` + stanza, and the validation regex will screen out invalid paths in the same way. + Even with the validation, implementations MUST sanitize this input before using it + directly. + maxLength: 1024 + pattern: ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$ + type: string + type: object + protocol: + description: |- + ExternalAuthProtocol describes which protocol to use when communicating with an + ext_authz authorization server. + + When this is set to GRPC, each backend must use the Envoy ext_authz protocol + on the port specified in `backendRefs`. Requests and responses are defined + in the protobufs explained at: + https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto + + When this is set to HTTP, each backend must respond with a `200` status + code in on a successful authorization. Any other code is considered + an authorization failure. + + Feature Names: + GRPC Support - HTTPRouteExternalAuthGRPC + HTTP Support - HTTPRouteExternalAuthHTTP + enum: + - HTTP + - GRPC + type: string + required: + - backendRef + - protocol + type: object + x-kubernetes-validations: + - message: grpc must be specified when protocol is set + to 'GRPC' + rule: 'self.protocol == ''GRPC'' ? has(self.grpc) : + true' + - message: protocol must be 'GRPC' when grpc is set + rule: 'has(self.grpc) ? self.protocol == ''GRPC'' : + true' + - message: http must be specified when protocol is set + to 'HTTP' + rule: 'self.protocol == ''HTTP'' ? has(self.http) : + true' + - message: protocol must be 'HTTP' when http is set + rule: 'has(self.http) ? self.protocol == ''HTTP'' : + true' requestHeaderModifier: description: |- RequestHeaderModifier defines a schema for a filter that modifies request @@ -10829,7 +14422,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -10841,10 +14434,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -10903,7 +14500,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -10915,10 +14512,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -10931,7 +14532,7 @@ spec: x-kubernetes-list-type: map type: object requestMirror: - description: |+ + description: |- RequestMirror defines a schema for a filter that mirrors requests. Requests are sent to the specified destination, but responses from that destination are ignored. @@ -10941,7 +14542,6 @@ spec: backends. Support: Extended - properties: backendRef: description: |- @@ -11037,13 +14637,12 @@ spec: rule: '(size(self.group) == 0 && self.kind == ''Service'') ? has(self.port) : true' fraction: - description: |+ + description: |- Fraction represents the fraction of requests that should be mirrored to BackendRef. Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - properties: denominator: default: 100 @@ -11062,14 +14661,13 @@ spec: denominator rule: self.numerator <= self.denominator percent: - description: |+ + description: |- Percent represents the percentage of requests that should be mirrored to BackendRef. Its minimum value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of requests). Only one of Fraction or Percent may be specified. If neither field is specified, 100% of requests will be mirrored. - format: int32 maximum: 100 minimum: 0 @@ -11232,6 +14830,9 @@ spec: enum: - 301 - 302 + - 303 + - 307 + - 308 type: integer type: object responseHeaderModifier: @@ -11266,7 +14867,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -11278,10 +14879,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -11340,7 +14945,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, the first entry with an equivalent name MUST be considered for a match. Subsequent entries @@ -11352,10 +14957,14 @@ spec: pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ type: string value: - description: Value is the value of HTTP Header - to be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -11408,6 +15017,8 @@ spec: - RequestRedirect - URLRewrite - ExtensionRef + - CORS + - ExternalAuth type: string urlRewrite: description: |- @@ -11497,6 +15108,11 @@ spec: - type type: object x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type is not + CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' - message: filter.requestHeaderModifier must be nil if the filter.type is not RequestHeaderModifier rule: '!(has(self.requestHeaderModifier) && self.type != @@ -11537,13 +15153,22 @@ spec: - message: filter.extensionRef must be specified for ExtensionRef filter.type rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + - message: filter.externalAuth must be nil if the filter.type + is not ExternalAuth + rule: '!(has(self.externalAuth) && self.type != ''ExternalAuth'')' + - message: filter.externalAuth must be specified for ExternalAuth + filter.type + rule: '!(!has(self.externalAuth) && self.type == ''ExternalAuth'')' maxItems: 16 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both rule: '!(self.exists(f, f.type == ''RequestRedirect'') && self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 - message: RequestHeaderModifier filter cannot be repeated rule: self.filter(f, f.type == 'RequestHeaderModifier').size() <= 1 @@ -11640,7 +15265,7 @@ spec: name: description: |- Name is the name of the HTTP Header to be matched. Name matching MUST be - case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). If multiple entries specify equivalent header names, only the first entry with an equivalent name MUST be considered for a match. Subsequent @@ -11675,10 +15300,14 @@ spec: - RegularExpression type: string value: - description: Value is the value of HTTP Header to - be matched. + description: |- + Value is the value of HTTP Header to be matched. + + Must consist of printable US-ASCII characters, optionally separated + by single tabs or spaces. See: https://tools.ietf.org/html/rfc7230#section-3.2 maxLength: 4096 minLength: 1 + pattern: ^[!-~]+([\t ]?[!-~]+)*$ type: string required: - name @@ -11850,8 +15479,9 @@ spec: type: object maxItems: 64 type: array + x-kubernetes-list-type: atomic name: - description: | + description: |- Name is the name of the route rule. This name MUST be unique within a Route if it is set. Support: Extended @@ -11860,15 +15490,14 @@ spec: pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string retry: - description: |+ + description: |- Retry defines the configuration for when to retry an HTTP request. Support: Extended - properties: attempts: description: |- - Attempts specifies the maxmimum number of times an individual request + Attempts specifies the maximum number of times an individual request from the gateway to a backend should be retried. If the maximum number of retries has been attempted without a successful @@ -11887,7 +15516,7 @@ spec: For example, setting the `rules[].retry.backoff` field to the value `100ms` will cause a backend request to first be retried approximately 100 milliseconds after timing out or receiving a response code configured - to be retryable. + to be retriable. An implementation MAY use an exponential or alternative backoff strategy for subsequent retry attempts, MAY cap the maximum backoff duration to @@ -11930,7 +15559,7 @@ spec: HTTPRouteRetryStatusCode defines an HTTP response status code for which a backend request should be retried. - Implementations MUST support the following status codes as retryable: + Implementations MUST support the following status codes as retriable: * 500 * 502 @@ -11942,20 +15571,18 @@ spec: Implementations MAY support specifying discrete values in the 400-499 range, which are often inadvisable to retry. - - maximum: 599 minimum: 400 type: integer type: array + x-kubernetes-list-type: atomic type: object sessionPersistence: - description: |+ + description: |- SessionPersistence defines and configures session persistence for the route rule. Support: Extended - properties: absoluteTimeout: description: |- @@ -11990,6 +15617,8 @@ spec: absolute lifetime of the cookie tracked by the gateway and is optional. + Defaults to "Session". + Support: Core for "Session" type Support: Extended for "Permanent" type @@ -12021,7 +15650,7 @@ spec: default: Cookie description: |- Type defines the type of session persistence such as through - the use a header or cookie. Defaults to cookie based session + the use of a header or cookie. Defaults to cookie based session persistence. Support: Core for "Cookie" type @@ -12037,6 +15666,8 @@ spec: is Permanent rule: '!has(self.cookieConfig) || !has(self.cookieConfig.lifetimeType) || self.cookieConfig.lifetimeType != ''Permanent'' || has(self.absoluteTimeout)' + - message: cookieConfig can only be set with type Cookie + rule: '!has(self.cookieConfig) || self.type == ''Cookie''' timeouts: description: |- Timeouts defines the timeouts that can be configured for an HTTP request. @@ -12141,7 +15772,9 @@ spec: != 1 || !has(self.matches[0].path) || self.matches[0].path.type != ''PathPrefix'') ? false : true) : true' maxItems: 16 + minItems: 1 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: While 16 rules and 64 matches per rule are allowed, the total number of matches across all rules in a route must be less @@ -12160,6 +15793,24 @@ spec: - message: Rule name must be unique within the route rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name)) + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string type: object status: description: Status defines the current state of HTTPRoute. @@ -12203,9 +15854,9 @@ spec: There are a number of cases where the "Accepted" condition may not be set due to lack of controller visibility, that includes when: - * The Route refers to a non-existent parent. + * The Route refers to a nonexistent parent. * The Route is of a type that the controller does not support. - * The Route is in a namespace the controller does not have access to. + * The Route is in a namespace to which the controller does not have access. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -12424,11 +16075,13 @@ spec: - name type: object required: + - conditions - controllerName - parentRef type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic required: - parents type: object @@ -12450,10 +16103,790 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 + gateway.networking.k8s.io/channel: experimental + name: listenersets.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: ListenerSet + listKind: ListenerSetList + plural: listenersets + shortNames: + - lset + singular: listenerset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ListenerSet defines a set of additional listeners to attach to an existing Gateway. + This resource provides a mechanism to merge multiple listeners into a single Gateway. + + The parent Gateway must explicitly allow ListenerSet attachment through its + AllowedListeners configuration. By default, Gateways do not allow ListenerSet + attachment. + + Routes can attach to a ListenerSet by specifying it as a parentRef, and can + optionally target specific listeners using the sectionName field. + + Policy Attachment: + - Policies that attach to a ListenerSet apply to all listeners defined in that resource + - Policies do not impact listeners in the parent Gateway + - Different ListenerSets attached to the same Gateway can have different policies + - If an implementation cannot apply a policy to specific listeners, it should reject the policy + + ReferenceGrant Semantics: + - ReferenceGrants applied to a Gateway are not inherited by child ListenerSets + - ReferenceGrants applied to a ListenerSet do not grant permission to the parent Gateway's listeners + - A ListenerSet can reference secrets/backends in its own namespace without a ReferenceGrant + + Gateway Integration: + - The parent Gateway's status will include "AttachedListenerSets" + which is the count of ListenerSets that have successfully attached to a Gateway + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ListenerSet. + properties: + listeners: + description: |- + Listeners associated with this ListenerSet. Listeners define + logical endpoints that are bound on this referenced parent Gateway's addresses. + + Listeners in a `Gateway` and their attached `ListenerSets` are concatenated + as a list when programming the underlying infrastructure. Each listener + name does not need to be unique across the Gateway and ListenerSets. + See ListenerEntry.Name for more details. + + Implementations MUST treat the parent Gateway as having the merged + list of all listeners from itself and attached ListenerSets using + the following precedence: + + 1. "parent" Gateway + 2. ListenerSet ordered by creation time (oldest first) + 3. ListenerSet ordered alphabetically by "{namespace}/{name}". + + An implementation MAY reject listeners by setting the ListenerEntryStatus + `Accepted` condition to False with the Reason `TooManyListeners` + + If a listener has a conflict, this will be reported in the + Status.ListenerEntryStatus setting the `Conflicted` condition to True. + + Implementations SHOULD be cautious about what information from the + parent or siblings are reported to avoid accidentally leaking + sensitive information that the child would not otherwise have access + to. This can include contents of secrets etc. + items: + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match at both the TLS and HTTP + protocol layers as described above. If an implementation does not + ensure that both the SNI and Host header match the Listener hostname, + it MUST clearly document that. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + ListenerSet. + + Name is not required to be unique across a Gateway and ListenerSets. + Routes can attach to a Listener by having a ListenerSet as a parentRef + and setting the SectionName + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: Protocol specifies the network protocol this listener + expects to receive. + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, !has(l1.port) || self.exists_one(l2, has(l2.port) + && l1.port == l2.port && l1.protocol == l2.protocol && (has(l1.hostname) + && has(l2.hostname) ? l1.hostname == l2.hostname : !has(l1.hostname) + && !has(l2.hostname))))' + parentRef: + description: ParentRef references the Gateway that the listeners are + attached to. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: Kind is kind of the referent. For example "Gateway". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. If not present, + the namespace of the referent is assumed to be the same as + the namespace of the referring object. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + required: + - listeners + - parentRef + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of ListenerSet. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the ListenerSet. + + Implementations MUST express ListenerSet conditions using the + `ListenerSetConditionType` and `ListenerSetConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe ListenerSet state. + + Known condition types are: + + * "Accepted" + * "Programmed" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: referencegrants.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -12468,6 +16901,169 @@ spec: singular: referencegrant scope: Namespaced versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ReferenceGrant identifies kinds of resources in other namespaces that are + trusted to reference the specified kinds of resources in the same namespace + as the policy. + + Each ReferenceGrant can be used to represent a unique trust relationship. + Additional Reference Grants can be used to add to the set of trusted + sources of inbound references for the namespace they are defined within. + + All cross-namespace references in Gateway API (with the exception of cross-namespace + Gateway-route attachment) require a ReferenceGrant. + + ReferenceGrant is a form of runtime verification allowing users to assert + which cross-namespace object references are permitted. Implementations that + support ReferenceGrant MUST NOT permit cross-namespace references which have + no grant, and MUST respond to the removal of a grant by revoking the access + that the grant allowed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ReferenceGrant. + properties: + from: + description: |- + From describes the trusted namespaces and kinds that can reference the + resources described in "To". Each entry in this list MUST be considered + to be an additional place that references can be valid from, or to put + this another way, entries MUST be combined using OR. + + Support: Core + items: + description: ReferenceGrantFrom describes trusted namespaces and + kinds. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field. + + When used to permit a SecretObjectReference: + + * Gateway + + When used to permit a BackendObjectReference: + + * GRPCRoute + * HTTPRoute + * TCPRoute + * TLSRoute + * UDPRoute + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - namespace + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + to: + description: |- + To describes the resources that may be referenced by the resources + described in "From". Each entry in this list MUST be considered to be an + additional place that references can be valid to, or to put this another + way, entries MUST be combined using OR. + + Support: Core + items: + description: |- + ReferenceGrantTo describes what Kinds are allowed as targets of the + references. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field: + + * Secret when used to permit a SecretObjectReference + * Service when used to permit a BackendObjectReference + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. When unspecified, this policy + refers to all resources of the specified Group and Kind in the local + namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - from + - to + type: object + type: object + served: true + storage: false + subresources: {} - additionalPrinterColumns: - jsonPath: .metadata.creationTimestamp name: Age @@ -12572,6 +17168,7 @@ spec: maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic to: description: |- To describes the resources that may be referenced by the resources @@ -12621,6 +17218,7 @@ spec: maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic required: - from - to @@ -12640,10 +17238,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: tcproutes.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -12689,7 +17286,7 @@ spec: description: Spec defines the desired state of TCPRoute. properties: parentRefs: - description: |+ + description: |- ParentRefs references the resources (usually Gateways) that a Route wants to be attached to. Note that the referenced parent resource needs to allow this for the attachment to be complete. For Gateways, that means @@ -12751,11 +17348,6 @@ spec: connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. - - - - - items: description: |- ParentReference identifies an API object (usually a Gateway) that can be considered @@ -12905,6 +17497,7 @@ spec: type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: sectionName or port must be specified when parentRefs includes 2 or more references to the same parent @@ -12929,16 +17522,14 @@ spec: || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port == p2.port)))) rules: - description: |+ - Rules are a list of TCP matchers and actions. - + description: Rules are a list of TCP matchers and actions. items: description: TCPRouteRule is the configuration for a given rule. properties: backendRefs: description: |- BackendRefs defines the backend(s) where matching requests should be - sent. If unspecified or invalid (refers to a non-existent resource or a + sent. If unspecified or invalid (refers to a nonexistent resource or a Service with no endpoints), the underlying implementation MUST actively reject connection attempts to this backend. Connection rejections must respect weight; if an invalid backend is requested to have 80% of @@ -12961,7 +17552,6 @@ spec: namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - When the BackendRef points to a Kubernetes Service, implementations SHOULD honor the appProtocol field if it is set for the target Service Port. @@ -12977,7 +17567,6 @@ spec: protocol then the backend is considered invalid. Implementations MUST set the "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. - Note that when the BackendTLSPolicy object is enabled by the implementation, there are some extra rules about validity to consider here. See the fields @@ -13073,6 +17662,7 @@ spec: maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic name: description: |- Name is the name of the route rule. This name MUST be unique within a Route if it is set. @@ -13082,14 +17672,35 @@ spec: minLength: 1 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + required: + - backendRefs type: object maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: Rule name must be unique within the route rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name)) + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string required: - rules type: object @@ -13135,9 +17746,9 @@ spec: There are a number of cases where the "Accepted" condition may not be set due to lack of controller visibility, that includes when: - * The Route refers to a non-existent parent. + * The Route refers to a nonexistent parent. * The Route is of a type that the controller does not support. - * The Route is in a namespace the controller does not have access to. + * The Route is in a namespace to which the controller does not have access. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -13356,11 +17967,13 @@ spec: - name type: object required: + - conditions - controllerName - parentRef type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic required: - parents type: object @@ -13382,10 +17995,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 gateway.networking.k8s.io/channel: experimental - creationTimestamp: null name: tlsroutes.gateway.networking.k8s.io spec: group: gateway.networking.k8s.io @@ -13402,7 +18014,7 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date - name: v1alpha2 + name: v1 schema: openAPIV3Schema: description: |- @@ -13412,6 +18024,781 @@ spec: If you need to forward traffic to a single target for a TLS listener, you could choose to use a TCPRoute with a TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI hostnames that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI hostnames per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Hostnames cannot contain an IP + rule: self.all(h, !isIP(h)) + - message: Hostnames must be valid based on RFC-1123 + rule: 'self.all(h, !h.contains(''*'') ? h.matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'') + : true)' + - message: Wildcards on hostnames must be the first label, and the + rest of hostname must be valid based on RFC-1123 + rule: 'self.all(h, h.contains(''*'') ? (h.startsWith(''*.'') && + h.substring(2).matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'')) + : true)' + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName or port must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__)) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''') && (!has(p1.port) || p1.port == 0) == (!has(p2.port) + || p2.port == 0)): true))' + - message: sectionName or port must be unique when parentRefs includes + 2 or more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || ( has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)) && (((!has(p1.port) || p1.port == 0) && (!has(p2.port) + || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port + == p2.port)))) + rules: + description: Rules are a list of actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + + When the BackendRef points to a Kubernetes Service, implementations SHOULD + honor the appProtocol field if it is set for the target Service Port. + + Implementations supporting appProtocol SHOULD recognize the Kubernetes + Standard Application Protocols defined in KEP-3726. + + If a Service appProtocol isn't specified, an implementation MAY infer the + backend protocol through its own means. Implementations MAY infer the + protocol from the Route type referring to the backend Service. + + If a Route is not able to send traffic to the backend using the specified + protocol then the backend is considered invalid. Implementations MUST set the + "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. + + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string + required: + - hostnames + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of TLSRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. properties: apiVersion: description: |- @@ -13490,8 +18877,9 @@ spec: type: string maxItems: 16 type: array + x-kubernetes-list-type: atomic parentRefs: - description: |+ + description: |- ParentRefs references the resources (usually Gateways) that a Route wants to be attached to. Note that the referenced parent resource needs to allow this for the attachment to be complete. For Gateways, that means @@ -13553,11 +18941,6 @@ spec: connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. - - - - - items: description: |- ParentReference identifies an API object (usually a Gateway) that can be considered @@ -13707,6 +19090,7 @@ spec: type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: sectionName or port must be specified when parentRefs includes 2 or more references to the same parent @@ -13731,23 +19115,20 @@ spec: || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port == p2.port)))) rules: - description: |+ - Rules are a list of TLS matchers and actions. - + description: Rules are a list of TLS matchers and actions. items: description: TLSRouteRule is the configuration for a given rule. properties: backendRefs: description: |- BackendRefs defines the backend(s) where matching requests should be - sent. If unspecified or invalid (refers to a non-existent resource or + sent. If unspecified or invalid (refers to a nonexistent resource or a Service with no endpoints), the rule performs no forwarding; if no filters are specified that would result in a response being sent, the underlying implementation must actively reject request attempts to this - backend, by rejecting the connection or returning a 500 status code. - Request rejections must respect weight; if an invalid backend is - requested to have 80% of requests, then 80% of requests must be rejected - instead. + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. Support: Core for Kubernetes Service @@ -13766,7 +19147,6 @@ spec: namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - When the BackendRef points to a Kubernetes Service, implementations SHOULD honor the appProtocol field if it is set for the target Service Port. @@ -13782,7 +19162,6 @@ spec: protocol then the backend is considered invalid. Implementations MUST set the "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. - Note that when the BackendTLSPolicy object is enabled by the implementation, there are some extra rules about validity to consider here. See the fields @@ -13878,23 +19257,43 @@ spec: maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic name: - description: |- - Name is the name of the route rule. This name MUST be unique within a Route if it is set. - - Support: Extended + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. maxLength: 253 minLength: 1 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + required: + - backendRefs type: object maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: Rule name must be unique within the route rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name)) + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string required: - rules type: object @@ -13940,9 +19339,9 @@ spec: There are a number of cases where the "Accepted" condition may not be set due to lack of controller visibility, that includes when: - * The Route refers to a non-existent parent. + * The Route refers to a nonexistent parent. * The Route is of a type that the controller does not support. - * The Route is in a namespace the controller does not have access to. + * The Route is in a namespace to which the controller does not have access. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -14161,11 +19560,13 @@ spec: - name type: object required: + - conditions - controllerName - parentRef type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic required: - parents type: object @@ -14173,47 +19574,26 @@ spec: - spec type: object served: true - storage: true + storage: false subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/3328 - gateway.networking.k8s.io/bundle-version: v1.2.0 - gateway.networking.k8s.io/channel: experimental - creationTimestamp: null - name: udproutes.gateway.networking.k8s.io -spec: - group: gateway.networking.k8s.io - names: - categories: - - gateway-api - kind: UDPRoute - listKind: UDPRouteList - plural: udproutes - singular: udproute - scope: Namespaced - versions: - additionalPrinterColumns: - jsonPath: .metadata.creationTimestamp name: Age type: date - name: v1alpha2 + deprecated: true + deprecationWarning: The v1alpha3 version of TLSRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha3 schema: openAPIV3Schema: description: |- - UDPRoute provides a way to route UDP traffic. When combined with a Gateway - listener, it can be used to forward traffic on the port specified by the - listener to a set of backends specified by the UDPRoute. + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + + If you need to forward traffic to a single target for a TLS listener, you + could choose to use a TCPRoute with a TLS listener. properties: apiVersion: description: |- @@ -14233,10 +19613,54 @@ spec: metadata: type: object spec: - description: Spec defines the desired state of UDPRoute. + description: Spec defines the desired state of TLSRoute. properties: + hostnames: + description: |- + Hostnames defines a set of SNI hostnames that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI hostnames per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Hostnames cannot contain an IP + rule: self.all(h, !isIP(h)) + - message: Hostnames must be valid based on RFC-1123 + rule: 'self.all(h, !h.contains(''*'') ? h.matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'') + : true)' + - message: Wildcards on hostnames must be the first label, and the + rest of hostname must be valid based on RFC-1123 + rule: 'self.all(h, h.contains(''*'') ? (h.startsWith(''*.'') && + h.substring(2).matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'')) + : true)' parentRefs: - description: |+ + description: |- ParentRefs references the resources (usually Gateways) that a Route wants to be attached to. Note that the referenced parent resource needs to allow this for the attachment to be complete. For Gateways, that means @@ -14298,11 +19722,6 @@ spec: connections originating from the same namespace as the Route, for which the intended destination of the connections are a Service targeted as a ParentRef of the Route. - - - - - items: description: |- ParentReference identifies an API object (usually a Gateway) that can be considered @@ -14452,6 +19871,7 @@ spec: type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic x-kubernetes-validations: - message: sectionName or port must be specified when parentRefs includes 2 or more references to the same parent @@ -14476,20 +19896,20 @@ spec: || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port == p2.port)))) rules: - description: |+ - Rules are a list of UDP matchers and actions. - + description: Rules are a list of actions. items: - description: UDPRouteRule is the configuration for a given rule. + description: TLSRouteRule is the configuration for a given rule. properties: backendRefs: description: |- BackendRefs defines the backend(s) where matching requests should be - sent. If unspecified or invalid (refers to a non-existent resource or a - Service with no endpoints), the underlying implementation MUST actively - reject connection attempts to this backend. Packet drops must - respect weight; if an invalid backend is requested to have 80% of - the packets, then 80% of packets must be dropped instead. + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. Support: Core for Kubernetes Service @@ -14508,7 +19928,6 @@ spec: namespace's owner to accept the reference. See the ReferenceGrant documentation for details. - When the BackendRef points to a Kubernetes Service, implementations SHOULD honor the appProtocol field if it is set for the target Service Port. @@ -14524,7 +19943,6 @@ spec: protocol then the backend is considered invalid. Implementations MUST set the "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. - Note that when the BackendTLSPolicy object is enabled by the implementation, there are some extra rules about validity to consider here. See the fields @@ -14620,28 +20038,45 @@ spec: maxItems: 16 minItems: 1 type: array + x-kubernetes-list-type: atomic name: - description: |- - Name is the name of the route rule. This name MUST be unique within a Route if it is set. - - Support: Extended + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. maxLength: 253 minLength: 1 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + required: + - backendRefs type: object - maxItems: 16 + maxItems: 1 minItems: 1 type: array - x-kubernetes-validations: - - message: Rule name must be unique within the route - rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) - && l1.name == l2.name)) + x-kubernetes-list-type: atomic + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string required: + - hostnames - rules type: object status: - description: Status defines the current state of UDPRoute. + description: Status defines the current state of TLSRoute. properties: parents: description: |- @@ -14682,9 +20117,9 @@ spec: There are a number of cases where the "Accepted" condition may not be set due to lack of controller visibility, that includes when: - * The Route refers to a non-existent parent. + * The Route refers to a nonexistent parent. * The Route is of a type that the controller does not support. - * The Route is in a namespace the controller does not have access to. + * The Route is in a namespace to which the controller does not have access. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -14903,11 +20338,770 @@ spec: - name type: object required: + - conditions - controllerName - parentRef type: object maxItems: 32 type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 + gateway.networking.k8s.io/channel: experimental + name: udproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: UDPRoute + listKind: UDPRouteList + plural: udproutes + singular: udproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + UDPRoute provides a way to route UDP traffic. When combined with a Gateway + listener, it can be used to forward traffic on the port specified by the + listener to a set of backends specified by the UDPRoute. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of UDPRoute. + properties: + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName or port must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__)) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''') && (!has(p1.port) || p1.port == 0) == (!has(p2.port) + || p2.port == 0)): true))' + - message: sectionName or port must be unique when parentRefs includes + 2 or more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || ( has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)) && (((!has(p1.port) || p1.port == 0) && (!has(p2.port) + || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port + == p2.port)))) + rules: + description: Rules are a list of UDP matchers and actions. + items: + description: UDPRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or a + Service with no endpoints), the underlying implementation MUST actively + reject connection attempts to this backend. Packet drops must + respect weight; if an invalid backend is requested to have 80% of + the packets, then 80% of packets must be dropped instead. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + + When the BackendRef points to a Kubernetes Service, implementations SHOULD + honor the appProtocol field if it is set for the target Service Port. + + Implementations supporting appProtocol SHOULD recognize the Kubernetes + Standard Application Protocols defined in KEP-3726. + + If a Service appProtocol isn't specified, an implementation MAY infer the + backend protocol through its own means. Implementations MAY infer the + protocol from the Route type referring to the backend Service. + + If a Route is not able to send traffic to the backend using the specified + protocol then the backend is considered invalid. Implementations MUST set the + "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. + + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Rule name must be unique within the route + rule: self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) + && l1.name == l2.name)) + useDefaultGateways: + description: |- + UseDefaultGateways indicates the default Gateway scope to use for this + Route. If unset (the default) or set to None, the Route will not be + attached to any default Gateway; if set, it will be attached to any + default Gateway supporting the named scope, subject to the usual rules + about which Routes a Gateway is allowed to claim. + + Think carefully before using this functionality! The set of default + Gateways supporting the requested scope can change over time without + any notice to the Route author, and in many situations it will not be + appropriate to request a default Gateway for a given Route -- for + example, a Route with specific security requirements should almost + certainly not use a default Gateway. + enum: + - All + - None + type: string + required: + - rules + type: object + status: + description: Status defines the current state of UDPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic required: - parents type: object @@ -14924,3 +21118,926 @@ status: plural: "" conditions: null storedVersions: null +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 + gateway.networking.k8s.io/channel: experimental + labels: + gateway.networking.k8s.io/policy: Direct + name: xbackendtrafficpolicies.gateway.networking.x-k8s.io +spec: + group: gateway.networking.x-k8s.io + names: + categories: + - gateway-api + kind: XBackendTrafficPolicy + listKind: XBackendTrafficPolicyList + plural: xbackendtrafficpolicies + shortNames: + - xbtrafficpolicy + singular: xbackendtrafficpolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + XBackendTrafficPolicy defines the configuration for how traffic to a + target backend should be handled. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of BackendTrafficPolicy. + properties: + retryConstraint: + description: |- + RetryConstraint defines the configuration for when to allow or prevent + further retries to a target backend, by dynamically calculating a 'retry + budget'. This budget is calculated based on the percentage of incoming + traffic composed of retries over a given time interval. Once the budget + is exceeded, additional retries will be rejected. + + For example, if the retry budget interval is 10 seconds, there have been + 1000 active requests in the past 10 seconds, and the allowed percentage + of requests that can be retried is 20% (the default), then 200 of those + requests may be composed of retries. Active requests will only be + considered for the duration of the interval when calculating the retry + budget. Retrying the same original request multiple times within the + retry budget interval will lead to each retry being counted towards + calculating the budget. + + Configuring a RetryConstraint in BackendTrafficPolicy is compatible with + HTTPRoute Retry settings for each HTTPRouteRule that targets the same + backend. While the HTTPRouteRule Retry stanza can specify whether a + request will be retried, and the number of retry attempts each client + may perform, RetryConstraint helps prevent cascading failures such as + retry storms during periods of consistent failures. + + After the retry budget has been exceeded, additional retries to the + backend MUST return a 503 response to the client. + + Additional configurations for defining a constraint on retries MAY be + defined in the future. + + Support: Extended + properties: + budget: + default: + interval: 10s + percent: 20 + description: Budget holds the details of the retry budget configuration. + properties: + interval: + default: 10s + description: |- + Interval defines the duration in which requests will be considered + for calculating the budget for retries. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + x-kubernetes-validations: + - message: interval cannot be greater than one hour or less + than one second + rule: '!(duration(self) < duration(''1s'') || duration(self) + > duration(''1h''))' + percent: + default: 20 + description: |- + Percent defines the maximum percentage of active requests that may + be made up of retries. + + Support: Extended + maximum: 100 + minimum: 0 + type: integer + type: object + minRetryRate: + default: + count: 10 + interval: 1s + description: |- + MinRetryRate defines the minimum rate of retries that will be allowable + over a specified duration of time. + + The effective overall minimum rate of retries targeting the backend + service may be much higher, as there can be any number of clients which + are applying this setting locally. + + This ensures that requests can still be retried during periods of low + traffic, where the budget for retries may be calculated as a very low + value. + + Support: Extended + properties: + count: + description: |- + Count specifies the number of requests per time interval. + + Support: Extended + maximum: 1000000 + minimum: 1 + type: integer + interval: + description: |- + Interval specifies the divisor of the rate of requests, the amount of + time during which the given count of requests occur. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + x-kubernetes-validations: + - message: interval cannot be greater than one hour + rule: '!(duration(self) == duration(''0s'') || duration(self) + > duration(''1h''))' + type: object + type: object + sessionPersistence: + description: |- + SessionPersistence defines and configures session persistence + for the backend. + + Support: Extended + properties: + absoluteTimeout: + description: |- + AbsoluteTimeout defines the absolute timeout of the persistent + session. Once the AbsoluteTimeout duration has elapsed, the + session becomes invalid. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + cookieConfig: + description: |- + CookieConfig provides configuration settings that are specific + to cookie-based session persistence. + + Support: Core + properties: + lifetimeType: + default: Session + description: |- + LifetimeType specifies whether the cookie has a permanent or + session-based lifetime. A permanent cookie persists until its + specified expiry time, defined by the Expires or Max-Age cookie + attributes, while a session cookie is deleted when the current + session ends. + + When set to "Permanent", AbsoluteTimeout indicates the + cookie's lifetime via the Expires or Max-Age cookie attributes + and is required. + + When set to "Session", AbsoluteTimeout indicates the + absolute lifetime of the cookie tracked by the gateway and + is optional. + + Defaults to "Session". + + Support: Core for "Session" type + + Support: Extended for "Permanent" type + enum: + - Permanent + - Session + type: string + type: object + idleTimeout: + description: |- + IdleTimeout defines the idle timeout of the persistent session. + Once the session has been idle for more than the specified + IdleTimeout duration, the session becomes invalid. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + sessionName: + description: |- + SessionName defines the name of the persistent session token + which may be reflected in the cookie or the header. Users + should avoid reusing session names to prevent unintended + consequences, such as rejection or unpredictable behavior. + + Support: Implementation-specific + maxLength: 128 + type: string + type: + default: Cookie + description: |- + Type defines the type of session persistence such as through + the use of a header or cookie. Defaults to cookie based session + persistence. + + Support: Core for "Cookie" type + + Support: Extended for "Header" type + enum: + - Cookie + - Header + type: string + type: object + x-kubernetes-validations: + - message: AbsoluteTimeout must be specified when cookie lifetimeType + is Permanent + rule: '!has(self.cookieConfig) || !has(self.cookieConfig.lifetimeType) + || self.cookieConfig.lifetimeType != ''Permanent'' || has(self.absoluteTimeout)' + - message: cookieConfig can only be set with type Cookie + rule: '!has(self.cookieConfig) || self.type == ''Cookie''' + targetRefs: + description: |- + TargetRefs identifies API object(s) to apply this policy to. + Currently, Backends (A grouping of like endpoints such as Service, + ServiceImport, or any implementation-specific backendRef) are the only + valid API target references. + + Currently, a TargetRef cannot be scoped to a specific port on a + Service. + items: + description: |- + LocalPolicyTargetReference identifies an API object to apply a direct or + inherited policy to. This should be used as part of Policy resources + that can target Gateway API resources. For more information on how this + policy attachment model works, and a sample Policy resource, refer to + the policy attachment documentation for Gateway API. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - group + - kind + - name + x-kubernetes-list-type: map + required: + - targetRefs + type: object + status: + description: Status defines the current state of BackendTrafficPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - conditions + - controllerName + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + required: + - ancestors + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.1 + gateway.networking.k8s.io/channel: experimental + name: xmeshes.gateway.networking.x-k8s.io +spec: + group: gateway.networking.x-k8s.io + names: + categories: + - gateway-api + kind: XMesh + listKind: XMeshList + plural: xmeshes + shortNames: + - mesh + singular: xmesh + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: XMesh defines mesh-wide characteristics of a GAMMA-compliant + service mesh. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of XMesh. + properties: + controllerName: + description: |- + ControllerName is the name of a controller that is managing Gateway API + resources for mesh traffic management. The value of this field MUST be a + domain prefixed path. + + Example: "example.com/awesome-mesh". + + This field is not mutable and cannot be empty. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + description: + description: Description optionally provides a human-readable description + of a Mesh. + maxLength: 64 + type: string + parametersRef: + description: |- + ParametersRef is an optional reference to a resource that contains + implementation-specific configuration for this Mesh. If no + implementation-specific parameters are needed, this field MUST be + omitted. + + ParametersRef can reference a standard Kubernetes resource, i.e. + ConfigMap, or an implementation-specific custom resource. The resource + can be cluster-scoped or namespace-scoped. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Mesh MUST be rejected + with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + This field is required when referring to a Namespace-scoped resource and + MUST be unset when referring to a Cluster-scoped resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + required: + - controllerName + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: Status defines the current state of XMesh. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions is the current status from the controller for + this Mesh. + + Controllers should prefer to publish conditions using values + of MeshConditionType for the type of each Condition. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + supportedFeatures: + description: |- + SupportedFeatures is the set of features the Mesh support. + It MUST be sorted in ascending alphabetical order by the Name key. + items: + properties: + name: + description: |- + FeatureName is used to describe distinct features that are covered by + conformance tests. + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + annotations: + gateway.networking.k8s.io/bundle-version: v1.5.0-dev + gateway.networking.k8s.io/channel: standard + name: safe-upgrades.gateway.networking.k8s.io +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - apiextensions.k8s.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - '*' + validations: + - expression: object.spec.group != 'gateway.networking.k8s.io' || oldObject == null + || ( has(object.metadata.annotations) && object.metadata.annotations.exists(k, + k == 'gateway.networking.k8s.io/channel') && object.metadata.annotations['gateway.networking.k8s.io/channel'] + == 'standard' ) || ( oldObject != null && has(oldObject.metadata.annotations) + && oldObject.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') + && oldObject.metadata.annotations['gateway.networking.k8s.io/channel'] == 'experimental' + ) + message: Installing experimental CRDs on top of standard channel CRDs is prohibited + by default. Uninstall ValidatingAdmissionPolicy safe-upgrades.gateway.networking.k8s.io + to install experimental CRDs on top of standard channel CRDs. + reason: Invalid + - expression: object.spec.group != 'gateway.networking.k8s.io' || (has(object.metadata.annotations) + && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/bundle-version') + && !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], + 'v1.[0-4].\\d+') && !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], + 'v0')) + message: Installing CRDs with version before v1.5.0 is prohibited by default. + Uninstall ValidatingAdmissionPolicy safe-upgrades.gateway.networking.k8s.io + to install older versions. + reason: Invalid +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + annotations: + gateway.networking.k8s.io/bundle-version: v1.5.0-dev + gateway.networking.k8s.io/channel: standard + name: safe-upgrades.gateway.networking.k8s.io +spec: + matchResources: + resourceRules: + - apiGroups: + - apiextensions.k8s.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - customresourcedefinitions + policyName: safe-upgrades.gateway.networking.k8s.io + validationActions: + - Deny From 2eb7bccee4906333c41a4713130c7b2ab2e2d0db Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 16:50:47 +0300 Subject: [PATCH 02/54] feat(cilium): enable embedded envoy and Gateway API controller Flip envoy.enabled and gatewayAPI.enabled on the vendored Cilium chart. Cilium becomes the Gateway API implementation for Cozystack: cilium-envoy DaemonSet starts on each node (approximately 100MB RAM per node in steady state), and the Cilium operator begins reconciling gateway.networking.k8s.io resources and publishing the built-in 'cilium' GatewayClass. This value flip alone does not create any Gateway objects. The platform installs only an idle Envoy DaemonSet and a GatewayClass until per-tenant gateway manifests are actually created by the platform chart. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/cilium/values.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/system/cilium/values.yaml b/packages/system/cilium/values.yaml index 9aba7cb9..81d0e843 100644 --- a/packages/system/cilium/values.yaml +++ b/packages/system/cilium/values.yaml @@ -18,7 +18,9 @@ cilium: tag: 1.19.3 digest: "sha256:700f06f4803a838a8e830be5ace4650e3ad82bdefabfb2f4d110368d307a5efb" envoy: - enabled: false + enabled: true + gatewayAPI: + enabled: true rollOutCiliumPods: true operator: rollOutPods: true From 7f2354170eb251dffd7ab06fa55a96bd4185d55f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 16:52:55 +0300 Subject: [PATCH 03/54] feat(platform): install gateway-api-crds on the host cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register a dedicated cozystack.gateway-api-crds PackageSource and wire it into the system bundle before networking/cert-manager load. Without this the host cluster has no gateway.networking.k8s.io CRDs, Cilium's Gateway API controller cannot bootstrap (no GatewayClass registered), and cert-manager's gateway-shim controller silently disables Gateway API certificate automation. - New PackageSource cozystack.gateway-api-crds → installs packages/system/gateway-api-crds into namespace cozy-gateway-api. - cozystack.networking (variants cilium, cilium-kilo, cilium-generic, kubeovn-cilium, kubeovn-cilium-generic) dependsOn cozystack.gateway-api-crds so Cilium only starts once the CRDs are present. - cozystack.cert-manager dependsOn cozystack.gateway-api-crds, closing the startup race that the #2208 discussion flagged — cert-manager's gateway-shim discovers CRDs at process start, so racing with CRD install left Gateway API solvers permanently disabled until the pod was restarted. - system bundle loads cozystack.gateway-api-crds unconditionally for every variant (isp-full, isp-hosted, isp-full-generic). Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../core/platform/sources/cert-manager.yaml | 1 + .../platform/sources/gateway-api-crds.yaml | 20 +++++++++++++++++++ .../core/platform/sources/networking.yaml | 15 +++++++++----- .../platform/templates/bundles/system.yaml | 3 +++ 4 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 packages/core/platform/sources/gateway-api-crds.yaml diff --git a/packages/core/platform/sources/cert-manager.yaml b/packages/core/platform/sources/cert-manager.yaml index 0907021e..857e9a08 100644 --- a/packages/core/platform/sources/cert-manager.yaml +++ b/packages/core/platform/sources/cert-manager.yaml @@ -13,6 +13,7 @@ spec: - name: default dependsOn: - cozystack.networking + - cozystack.gateway-api-crds components: - name: cert-manager-crds path: system/cert-manager-crds diff --git a/packages/core/platform/sources/gateway-api-crds.yaml b/packages/core/platform/sources/gateway-api-crds.yaml new file mode 100644 index 00000000..f6bcff9f --- /dev/null +++ b/packages/core/platform/sources/gateway-api-crds.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: cozystack.gateway-api-crds +spec: + sourceRef: + kind: OCIRepository + name: cozystack-packages + namespace: cozy-system + path: / + variants: + - name: default + dependsOn: [] + components: + - name: gateway-api-crds + path: system/gateway-api-crds + install: + namespace: cozy-gateway-api + releaseName: gateway-api-crds diff --git a/packages/core/platform/sources/networking.yaml b/packages/core/platform/sources/networking.yaml index bece76f8..3a568f59 100644 --- a/packages/core/platform/sources/networking.yaml +++ b/packages/core/platform/sources/networking.yaml @@ -13,7 +13,8 @@ spec: - name: noop components: [] - name: cilium - dependsOn: [] + dependsOn: + - cozystack.gateway-api-crds components: - name: cilium path: system/cilium @@ -34,7 +35,8 @@ spec: dependsOn: - cilium - name: cilium-kilo - dependsOn: [] + dependsOn: + - cozystack.gateway-api-crds components: - name: cilium path: system/cilium @@ -60,7 +62,8 @@ spec: - cilium # Generic Cilium variant for non-Talos clusters (kubeadm, k3s, RKE2, etc.) - name: cilium-generic - dependsOn: [] + dependsOn: + - cozystack.gateway-api-crds components: - name: cilium path: system/cilium @@ -81,7 +84,8 @@ spec: dependsOn: - cilium - name: kubeovn-cilium - dependsOn: [] + dependsOn: + - cozystack.gateway-api-crds components: - name: cilium path: system/cilium @@ -112,7 +116,8 @@ spec: - cilium # Generic KubeOVN+Cilium variant for non-Talos clusters (kubeadm, k3s, RKE2, etc.) - name: kubeovn-cilium-generic - dependsOn: [] + dependsOn: + - cozystack.gateway-api-crds components: - name: cilium path: system/cilium diff --git a/packages/core/platform/templates/bundles/system.yaml b/packages/core/platform/templates/bundles/system.yaml index 43ea7266..c3bf5817 100644 --- a/packages/core/platform/templates/bundles/system.yaml +++ b/packages/core/platform/templates/bundles/system.yaml @@ -1,5 +1,8 @@ {{- if .Values.bundles.system.enabled }} +# Gateway API CRDs (consumed by Cilium Gateway API controller and cert-manager) +{{include "cozystack.platform.package.default" (list "cozystack.gateway-api-crds" $) }} + # Networking {{- if eq .Values.bundles.system.variant "isp-full" }} {{- $networkingComponents := dict -}} From 5dc416293fe83228909a71f8567df430b4673e6f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 17:00:18 +0300 Subject: [PATCH 04/54] feat(gateway): add per-tenant Gateway API package and tenant toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a full per-tenant Gateway API setup backed by Cilium. The publishing tenant (the one whose namespace matches publishing.ingressName) can opt into a Gateway via tenant.spec.gateway instead of or alongside the existing Service.spec.externalIPs-based ingress-nginx deployment. Package layout: - packages/extra/gateway/ — new Helm chart that renders, per release: * cert-manager Certificate for the apex and wildcard hostnames. * One Gateway with three listeners — HTTP:80 (ACME challenges and redirect), HTTPS:443 wildcard *., HTTPS:443 apex . Each listener uses allowedRoutes.namespaces.from=Selector keyed on label cozystack.io/gateway= so system services can attach HTTPRoutes cross-namespace without ReferenceGrants. * HTTPRoute http-to-https-redirect (301) on the HTTP listener. cert-manager's own ACME solver HTTPRoute for /.well-known/acme-challenge/ will take precedence via Gateway API's more-specific-path matching rules. * CiliumLoadBalancerIPPool scoped to the tenant namespace and the Gateway's cilium-gateway-cozystack Service, so the IPs from publishing.externalIPs get announced via LB IPAM. * Render-time guards: fails loudly if released outside the publishing namespace or if publishing.host is unset. - packages/apps/tenant/ — new values key 'gateway: false' and a conditional HelmRelease template that references the gateway ExternalArtifact when the tenant opts in. Mirrors the existing ingress toggle verbatim, including labels and Flux reconcile config. - packages/core/platform/sources/gateway-application.yaml — new PackageSource cozystack.gateway-application with a single 'default' variant that wires extra/gateway as a tenant module and dependsOn cozystack.gateway-api-crds + cozystack.cert-manager. - packages/core/platform/templates/bundles/system.yaml — loads cozystack.gateway-application in every system variant so the ExternalArtifact is materialised before any tenant tries to use it. - api/apps/v1alpha1/tenant/types.go, values.schema.json, README.md, packages/system/tenant-rd/cozyrds/tenant.yaml — regenerated to include the new Gateway bool field on TenantSpec. - packages/extra/gateway/tests/gateway_test.yaml — 6 helm-unittest cases covering the IPv4/IPv6/mixed IP paths, empty-entry filtering, the no-IPs-still-renders-Gateway path, the non-publishing-namespace guard, and the empty-host guard. Default behaviour is unchanged: tenants default to gateway=false and the platform only materialises the ExternalArtifact (no resources inside any tenant namespace unless it opts in). Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- api/apps/v1alpha1/tenant/types.go | 31 ++-- packages/apps/tenant/README.md | 1 + packages/apps/tenant/templates/gateway.yaml | 32 ++++ packages/apps/tenant/values.schema.json | 41 ++--- packages/apps/tenant/values.yaml | 3 + .../platform/sources/gateway-application.yaml | 23 +++ .../platform/templates/bundles/system.yaml | 1 + packages/extra/gateway/.helmignore | 2 + packages/extra/gateway/Chart.yaml | 5 + packages/extra/gateway/Makefile | 9 ++ packages/extra/gateway/README.md | 12 ++ packages/extra/gateway/charts/cozy-lib | 1 + packages/extra/gateway/config.json | 23 +++ .../gateway/templates/cilium-lb-pool.yaml | 24 +++ packages/extra/gateway/templates/gateway.yaml | 84 ++++++++++ .../extra/gateway/tests/gateway_test.yaml | 143 ++++++++++++++++++ packages/extra/gateway/values.schema.json | 11 ++ packages/extra/gateway/values.yaml | 6 + packages/system/tenant-rd/cozyrds/tenant.yaml | 4 +- 19 files changed, 423 insertions(+), 33 deletions(-) create mode 100644 packages/apps/tenant/templates/gateway.yaml create mode 100644 packages/core/platform/sources/gateway-application.yaml create mode 100644 packages/extra/gateway/.helmignore create mode 100644 packages/extra/gateway/Chart.yaml create mode 100644 packages/extra/gateway/Makefile create mode 100644 packages/extra/gateway/README.md create mode 120000 packages/extra/gateway/charts/cozy-lib create mode 100644 packages/extra/gateway/config.json create mode 100644 packages/extra/gateway/templates/cilium-lb-pool.yaml create mode 100644 packages/extra/gateway/templates/gateway.yaml create mode 100644 packages/extra/gateway/tests/gateway_test.yaml create mode 100644 packages/extra/gateway/values.schema.json create mode 100644 packages/extra/gateway/values.yaml diff --git a/api/apps/v1alpha1/tenant/types.go b/api/apps/v1alpha1/tenant/types.go index 60f8bb22..e94d9fe7 100644 --- a/api/apps/v1alpha1/tenant/types.go +++ b/api/apps/v1alpha1/tenant/types.go @@ -1,7 +1,10 @@ -// Code generated by values-gen. DO NOT EDIT. // +kubebuilder:object:generate=true -// +groupName=apps.cozystack.io +// +kubebuilder:object:root=true +// +groupName=values.helm.io + // +versionName=v1alpha1 + +// Code generated by values-gen. DO NOT EDIT. package tenant import ( @@ -9,7 +12,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +kubebuilder:object:root=true type Config struct { v1.TypeMeta `json:",inline"` v1.ObjectMeta `json:"metadata,omitempty"` @@ -17,25 +19,28 @@ type Config struct { } type ConfigSpec struct { - // The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). - // +kubebuilder:default:="" - Host string `json:"host,omitempty"` // Deploy own Etcd cluster. // +kubebuilder:default:=false Etcd bool `json:"etcd"` - // Deploy own Monitoring Stack. + // Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). // +kubebuilder:default:=false - Monitoring bool `json:"monitoring"` + Gateway bool `json:"gateway"` + // The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). + // +kubebuilder:default:="" + Host string `json:"host,omitempty"` // Deploy own Ingress Controller. // +kubebuilder:default:=false Ingress bool `json:"ingress"` - // Deploy own SeaweedFS. + // Deploy own Monitoring Stack. // +kubebuilder:default:=false - Seaweedfs bool `json:"seaweedfs"` - // The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. - // +kubebuilder:default:="" - SchedulingClass string `json:"schedulingClass,omitempty"` + Monitoring bool `json:"monitoring"` // Define resource quotas for the tenant. // +kubebuilder:default:={} ResourceQuotas map[string]resource.Quantity `json:"resourceQuotas,omitempty"` + // The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. + // +kubebuilder:default:="" + SchedulingClass string `json:"schedulingClass,omitempty"` + // Deploy own SeaweedFS. + // +kubebuilder:default:=false + Seaweedfs bool `json:"seaweedfs"` } diff --git a/packages/apps/tenant/README.md b/packages/apps/tenant/README.md index 96cf5c6e..7bad6fa5 100644 --- a/packages/apps/tenant/README.md +++ b/packages/apps/tenant/README.md @@ -80,6 +80,7 @@ tenant-u1 | `etcd` | Deploy own Etcd cluster. | `bool` | `false` | | `monitoring` | Deploy own Monitoring Stack. | `bool` | `false` | | `ingress` | Deploy own Ingress Controller. | `bool` | `false` | +| `gateway` | Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). | `bool` | `false` | | `seaweedfs` | Deploy own SeaweedFS. | `bool` | `false` | | `schedulingClass` | The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. | `string` | `""` | | `resourceQuotas` | Define resource quotas for the tenant. | `map[string]quantity` | `{}` | diff --git a/packages/apps/tenant/templates/gateway.yaml b/packages/apps/tenant/templates/gateway.yaml new file mode 100644 index 00000000..7dc9b733 --- /dev/null +++ b/packages/apps/tenant/templates/gateway.yaml @@ -0,0 +1,32 @@ +{{- if .Values.gateway }} +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: gateway + namespace: {{ include "tenant.name" . }} + labels: + sharding.fluxcd.io/key: tenants + internal.cozystack.io/tenantmodule: "true" + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + apps.cozystack.io/application.kind: Gateway + apps.cozystack.io/application.group: apps.cozystack.io + apps.cozystack.io/application.name: gateway +spec: + chartRef: + kind: ExternalArtifact + name: cozystack-gateway-application-default-gateway + namespace: cozy-system + interval: 5m + timeout: 10m + install: + remediation: + retries: -1 + upgrade: + force: true + remediation: + retries: -1 + valuesFrom: + - kind: Secret + name: cozystack-values +{{- end }} diff --git a/packages/apps/tenant/values.schema.json b/packages/apps/tenant/values.schema.json index 28d9ac1e..f36e7b69 100644 --- a/packages/apps/tenant/values.schema.json +++ b/packages/apps/tenant/values.schema.json @@ -2,13 +2,23 @@ "title": "Chart Values", "type": "object", "properties": { + "etcd": { + "description": "Deploy own Etcd cluster.", + "type": "boolean", + "default": false + }, + "gateway": { + "description": "Deploy own Gateway API Gateway (backed by Cilium Gateway API controller).", + "type": "boolean", + "default": false + }, "host": { "description": "The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).", "type": "string", "default": "" }, - "etcd": { - "description": "Deploy own Etcd cluster.", + "ingress": { + "description": "Deploy own Ingress Controller.", "type": "boolean", "default": false }, @@ -17,21 +27,6 @@ "type": "boolean", "default": false }, - "ingress": { - "description": "Deploy own Ingress Controller.", - "type": "boolean", - "default": false - }, - "seaweedfs": { - "description": "Deploy own SeaweedFS.", - "type": "boolean", - "default": false - }, - "schedulingClass": { - "description": "The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.", - "type": "string", - "default": "" - }, "resourceQuotas": { "description": "Define resource quotas for the tenant.", "type": "object", @@ -48,6 +43,16 @@ ], "x-kubernetes-int-or-string": true } + }, + "schedulingClass": { + "description": "The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.", + "type": "string", + "default": "" + }, + "seaweedfs": { + "description": "Deploy own SeaweedFS.", + "type": "boolean", + "default": false } } -} +} \ No newline at end of file diff --git a/packages/apps/tenant/values.yaml b/packages/apps/tenant/values.yaml index f9f8a999..d55ddcf9 100644 --- a/packages/apps/tenant/values.yaml +++ b/packages/apps/tenant/values.yaml @@ -14,6 +14,9 @@ monitoring: false ## @param {bool} ingress - Deploy own Ingress Controller. ingress: false +## @param {bool} gateway - Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). +gateway: false + ## @param {bool} seaweedfs - Deploy own SeaweedFS. seaweedfs: false diff --git a/packages/core/platform/sources/gateway-application.yaml b/packages/core/platform/sources/gateway-application.yaml new file mode 100644 index 00000000..a75c37b1 --- /dev/null +++ b/packages/core/platform/sources/gateway-application.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: cozystack.io/v1alpha1 +kind: PackageSource +metadata: + name: cozystack.gateway-application +spec: + sourceRef: + kind: OCIRepository + name: cozystack-packages + namespace: cozy-system + path: / + variants: + - name: default + dependsOn: + - cozystack.gateway-api-crds + - cozystack.cert-manager + libraries: + - name: cozy-lib + path: library/cozy-lib + components: + - name: gateway + path: extra/gateway + libraries: ["cozy-lib"] diff --git a/packages/core/platform/templates/bundles/system.yaml b/packages/core/platform/templates/bundles/system.yaml index c3bf5817..ec7ed834 100644 --- a/packages/core/platform/templates/bundles/system.yaml +++ b/packages/core/platform/templates/bundles/system.yaml @@ -129,6 +129,7 @@ {{- $_ := set $tenantComponents "tenant" (dict "values" $tenantClusterValues) }} {{include "cozystack.platform.package" (list "cozystack.tenant-application" "default" $ $tenantComponents) }} {{include "cozystack.platform.package.default" (list "cozystack.ingress-application" $) }} +{{include "cozystack.platform.package.default" (list "cozystack.gateway-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.seaweedfs-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.info-application" $) }} {{include "cozystack.platform.package.default" (list "cozystack.monitoring-application" $) }} diff --git a/packages/extra/gateway/.helmignore b/packages/extra/gateway/.helmignore new file mode 100644 index 00000000..ab636230 --- /dev/null +++ b/packages/extra/gateway/.helmignore @@ -0,0 +1,2 @@ +.helmignore +/Makefile diff --git a/packages/extra/gateway/Chart.yaml b/packages/extra/gateway/Chart.yaml new file mode 100644 index 00000000..6e42a379 --- /dev/null +++ b/packages/extra/gateway/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: gateway +description: Cozystack per-tenant Gateway API resource backed by Cilium +type: application +version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process diff --git a/packages/extra/gateway/Makefile b/packages/extra/gateway/Makefile new file mode 100644 index 00000000..f5d77613 --- /dev/null +++ b/packages/extra/gateway/Makefile @@ -0,0 +1,9 @@ +NAME=gateway + +include ../../../hack/package.mk + +generate: + cozyvalues-gen -v values.yaml -s values.schema.json -r README.md + +test: + helm unittest . diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md new file mode 100644 index 00000000..cec54018 --- /dev/null +++ b/packages/extra/gateway/README.md @@ -0,0 +1,12 @@ +# Cozystack Tenant Gateway + +Per-tenant Gateway API Gateway backed by Cilium. Installed automatically when `tenant.spec.gateway=true` on the publishing tenant. + +## Parameters + +### Common parameters + +| Name | Description | Type | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| `gatewayClassName` | GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class. | `string` | `cilium` | + diff --git a/packages/extra/gateway/charts/cozy-lib b/packages/extra/gateway/charts/cozy-lib new file mode 120000 index 00000000..e1813509 --- /dev/null +++ b/packages/extra/gateway/charts/cozy-lib @@ -0,0 +1 @@ +../../../library/cozy-lib \ No newline at end of file diff --git a/packages/extra/gateway/config.json b/packages/extra/gateway/config.json new file mode 100644 index 00000000..b2f956f3 --- /dev/null +++ b/packages/extra/gateway/config.json @@ -0,0 +1,23 @@ +{ + "comments": { + "format": "##" + }, + "tags": { + "param": "@param", + "section": "@section", + "descriptionStart": "@descriptionStart", + "descriptionEnd": "@descriptionEnd", + "skip": "@skip", + "extra": "@extra" + }, + "modifiers": { + "array": "array", + "object": "object", + "string": "string", + "nullable": "nullable", + "default": "default" + }, + "regexp": { + "paramsSectionTitle": "Parameters" + } +} diff --git a/packages/extra/gateway/templates/cilium-lb-pool.yaml b/packages/extra/gateway/templates/cilium-lb-pool.yaml new file mode 100644 index 00000000..98f4303b --- /dev/null +++ b/packages/extra/gateway/templates/cilium-lb-pool.yaml @@ -0,0 +1,24 @@ +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $exposeExternalIPs := (index .Values._cluster "expose-external-ips") | default "" | nospace }} +{{- $exposeIPsList := list }} +{{- range splitList "," $exposeExternalIPs }} + {{- $ip := . | trim }} + {{- if $ip }} + {{- $exposeIPsList = append $exposeIPsList $ip }} + {{- end }} +{{- end }} +{{- if and (eq $exposeIngress .Release.Namespace) $exposeIPsList }} +apiVersion: cilium.io/v2 +kind: CiliumLoadBalancerIPPool +metadata: + name: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway +spec: + blocks: + {{- range $exposeIPsList }} + - cidr: {{ . }}/{{ if contains ":" . }}128{{ else }}32{{ end }} + {{- end }} + serviceSelector: + matchLabels: + "io.kubernetes.service.namespace": {{ .Release.Namespace | quote }} + "gateway.networking.k8s.io/gateway-name": cozystack +{{- end }} diff --git a/packages/extra/gateway/templates/gateway.yaml b/packages/extra/gateway/templates/gateway.yaml new file mode 100644 index 00000000..c3fb0ee3 --- /dev/null +++ b/packages/extra/gateway/templates/gateway.yaml @@ -0,0 +1,84 @@ +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $host := (index .Values._cluster "host") | default "" }} +{{- $issuerName := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} +{{- if not (eq $exposeIngress .Release.Namespace) }} +{{- fail (printf "packages/extra/gateway is only meaningful in the publishing namespace %q; current release namespace is %q. Set tenant.spec.gateway=true on that tenant only." $exposeIngress .Release.Namespace) }} +{{- end }} +{{- if not $host }} +{{- fail "publishing.host must be set in the platform chart for packages/extra/gateway to render a wildcard TLS listener." }} +{{- end }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway-tls +spec: + secretName: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway-tls + issuerRef: + name: {{ $issuerName }} + kind: ClusterIssuer + dnsNames: + - {{ $host | quote }} + - {{ printf "*.%s" $host | quote }} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: cozystack + labels: + cozystack.io/gateway: {{ .Release.Namespace }} +spec: + gatewayClassName: {{ .Values.gatewayClassName | quote }} + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Selector + selector: + matchLabels: + cozystack.io/gateway: {{ .Release.Namespace }} + - name: https + protocol: HTTPS + port: 443 + hostname: {{ printf "*.%s" $host | quote }} + tls: + mode: Terminate + certificateRefs: + - name: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway-tls + allowedRoutes: + namespaces: + from: Selector + selector: + matchLabels: + cozystack.io/gateway: {{ .Release.Namespace }} + - name: https-apex + protocol: HTTPS + port: 443 + hostname: {{ $host | quote }} + tls: + mode: Terminate + certificateRefs: + - name: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway-tls + allowedRoutes: + namespaces: + from: Selector + selector: + matchLabels: + cozystack.io/gateway: {{ .Release.Namespace }} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-to-https-redirect +spec: + parentRefs: + - name: cozystack + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml new file mode 100644 index 00000000..7e1cfe0b --- /dev/null +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -0,0 +1,143 @@ +suite: tenant gateway +templates: + - templates/gateway.yaml + - templates/cilium-lb-pool.yaml + +release: + name: gateway + namespace: tenant-root + +tests: + - it: renders Certificate, Gateway, redirect HTTPRoute and CiliumLoadBalancerIPPool when IPs and host are set + set: + _cluster: + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10,192.0.2.11" + host: example.org + issuer-name: letsencrypt-prod + asserts: + - template: templates/gateway.yaml + hasDocuments: + count: 3 + - template: templates/gateway.yaml + documentIndex: 0 + equal: + path: kind + value: Certificate + - template: templates/gateway.yaml + documentIndex: 0 + equal: + path: spec.dnsNames + value: + - example.org + - "*.example.org" + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: kind + value: Gateway + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.gatewayClassName + value: cilium + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].name + value: http + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[1].hostname + value: "*.example.org" + - template: templates/gateway.yaml + documentIndex: 2 + equal: + path: kind + value: HTTPRoute + - template: templates/gateway.yaml + documentIndex: 2 + equal: + path: spec.rules[0].filters[0].requestRedirect.scheme + value: https + - template: templates/cilium-lb-pool.yaml + hasDocuments: + count: 1 + - template: templates/cilium-lb-pool.yaml + equal: + path: apiVersion + value: cilium.io/v2 + - template: templates/cilium-lb-pool.yaml + equal: + path: spec.blocks + value: + - cidr: 192.0.2.10/32 + - cidr: 192.0.2.11/32 + - template: templates/cilium-lb-pool.yaml + equal: + path: spec.serviceSelector.matchLabels["gateway.networking.k8s.io/gateway-name"] + value: cozystack + + - it: IPv6 external IPs use /128 CIDRs + set: + _cluster: + expose-ingress: tenant-root + expose-external-ips: "2001:db8::1,192.0.2.10" + host: example.org + asserts: + - template: templates/cilium-lb-pool.yaml + equal: + path: spec.blocks + value: + - cidr: 2001:db8::1/128 + - cidr: 192.0.2.10/32 + + - it: empty entries in expose-external-ips are filtered out + set: + _cluster: + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10,," + host: example.org + asserts: + - template: templates/cilium-lb-pool.yaml + equal: + path: spec.blocks + value: + - cidr: 192.0.2.10/32 + + - it: no LoadBalancer pool is rendered when externalIPs is empty but Gateway still renders + set: + _cluster: + expose-ingress: tenant-root + expose-external-ips: "" + host: example.org + asserts: + - template: templates/gateway.yaml + hasDocuments: + count: 3 + - template: templates/cilium-lb-pool.yaml + hasDocuments: + count: 0 + + - it: release in a non-publishing namespace fails with an explicit error + set: + _cluster: + expose-ingress: tenant-root + host: example.org + release: + namespace: tenant-other + asserts: + - template: templates/gateway.yaml + failedTemplate: + errorMessage: 'packages/extra/gateway is only meaningful in the publishing namespace "tenant-root"; current release namespace is "tenant-other". Set tenant.spec.gateway=true on that tenant only.' + + - it: empty host fails with explicit error + set: + _cluster: + expose-ingress: tenant-root + host: "" + asserts: + - template: templates/gateway.yaml + failedTemplate: + errorMessage: "publishing.host must be set in the platform chart for packages/extra/gateway to render a wildcard TLS listener." diff --git a/packages/extra/gateway/values.schema.json b/packages/extra/gateway/values.schema.json new file mode 100644 index 00000000..4ec44ff6 --- /dev/null +++ b/packages/extra/gateway/values.schema.json @@ -0,0 +1,11 @@ +{ + "title": "Chart Values", + "type": "object", + "properties": { + "gatewayClassName": { + "description": "GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class.", + "type": "string", + "default": "cilium" + } + } +} \ No newline at end of file diff --git a/packages/extra/gateway/values.yaml b/packages/extra/gateway/values.yaml new file mode 100644 index 00000000..7831f7f9 --- /dev/null +++ b/packages/extra/gateway/values.yaml @@ -0,0 +1,6 @@ +## +## @section Common parameters +## + +## @param {string} gatewayClassName - GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class. +gatewayClassName: cilium diff --git a/packages/system/tenant-rd/cozyrds/tenant.yaml b/packages/system/tenant-rd/cozyrds/tenant.yaml index 9af11af0..765fcef5 100644 --- a/packages/system/tenant-rd/cozyrds/tenant.yaml +++ b/packages/system/tenant-rd/cozyrds/tenant.yaml @@ -8,7 +8,7 @@ spec: singular: tenant plural: tenants openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"host":{"description":"The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).","type":"string","default":""},"etcd":{"description":"Deploy own Etcd cluster.","type":"boolean","default":false},"monitoring":{"description":"Deploy own Monitoring Stack.","type":"boolean","default":false},"ingress":{"description":"Deploy own Ingress Controller.","type":"boolean","default":false},"seaweedfs":{"description":"Deploy own SeaweedFS.","type":"boolean","default":false},"schedulingClass":{"description":"The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.","type":"string","default":""},"resourceQuotas":{"description":"Define resource quotas for the tenant.","type":"object","default":{},"additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}} + {"title":"Chart Values","type":"object","properties":{"etcd":{"description":"Deploy own Etcd cluster.","type":"boolean","default":false},"gateway":{"description":"Deploy own Gateway API Gateway (backed by Cilium Gateway API controller).","type":"boolean","default":false},"host":{"description":"The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).","type":"string","default":""},"ingress":{"description":"Deploy own Ingress Controller.","type":"boolean","default":false},"monitoring":{"description":"Deploy own Monitoring Stack.","type":"boolean","default":false},"resourceQuotas":{"description":"Define resource quotas for the tenant.","type":"object","default":{},"additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}},"schedulingClass":{"description":"The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.","type":"string","default":""},"seaweedfs":{"description":"Deploy own SeaweedFS.","type":"boolean","default":false}}} release: prefix: tenant- labels: @@ -23,7 +23,7 @@ spec: plural: Tenants description: Separated tenant namespace icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODdfMzQwMykiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY4N18zNDAzKSI+CjxwYXRoIGQ9Ik03MiAyOUM2Ni4zOTI2IDI5IDYxLjAxNDggMzEuMjM4OCA1Ny4wNDk3IDM1LjIyNEM1My4wODQ3IDM5LjIwOTEgNTAuODU3MSA0NC42MTQxIDUwLjg1NzEgNTAuMjVDNTAuODU3MSA1NS44ODU5IDUzLjA4NDcgNjEuMjkwOSA1Ny4wNDk3IDY1LjI3NkM2MS4wMTQ4IDY5LjI2MTIgNjYuMzkyNiA3MS41IDcyIDcxLjVDNzcuNjA3NCA3MS41IDgyLjk4NTIgNjkuMjYxMiA4Ni45NTAzIDY1LjI3NkM5MC45MTUzIDYxLjI5MDkgOTMuMTQyOSA1NS44ODU5IDkzLjE0MjkgNTAuMjVDOTMuMTQyOSA0NC42MTQxIDkwLjkxNTMgMzkuMjA5MSA4Ni45NTAzIDM1LjIyNEM4Mi45ODUyIDMxLjIzODggNzcuNjA3NCAyOSA3MiAyOVpNNjAuOTgyNiA4My4zMDM3QzYwLjQ1NCA4Mi41ODk4IDU5LjU5NTEgODIuMTkxNCA1OC43MTk2IDgyLjI3NDRDNDUuMzg5NyA4My43MzU0IDM1IDk1LjEwNzQgMzUgMTA4LjkwM0MzNSAxMTEuNzI2IDM3LjI3OTUgMTE0IDQwLjA3MSAxMTRIMTAzLjkyOUMxMDYuNzM3IDExNCAxMDkgMTExLjcwOSAxMDkgMTA4LjkwM0MxMDkgOTUuMTA3NCA5OC42MTAzIDgzLjc1MiA4NS4yNjM4IDgyLjI5MUM4NC4zODg0IDgyLjE5MTQgODMuNTI5NSA4Mi42MDY0IDgzLjAwMDkgODMuMzIwM0w3NC4wOTc4IDk1LjI0MDJDNzMuMDQwNiA5Ni42NTE0IDcwLjkyNjMgOTYuNjUxNCA2OS44NjkyIDk1LjI0MDJMNjAuOTY2MSA4My4zMjAzTDYwLjk4MjYgODMuMzAzN1oiIGZpbGw9ImJsYWNrIi8+CjwvZz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl82ODdfMzQwMyIgeDE9IjcyIiB5MT0iMTQ0IiB4Mj0iLTEuMjgxN2UtMDUiIHkyPSI0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDMEQ2RkYiLz4KPHN0b3Agb2Zmc2V0PSIwLjMiIHN0b3AtY29sb3I9IiNDNERBRkYiLz4KPHN0b3Agb2Zmc2V0PSIwLjY1IiBzdG9wLWNvbG9yPSIjRDNFOUZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0U5RkZGRiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzY4N18zNDAzIj4KPHJlY3Qgd2lkdGg9Ijc0IiBoZWlnaHQ9Ijg1IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzUgMjkpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg== - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "etcd"], ["spec", "monitoring"], ["spec", "ingress"], ["spec", "seaweedfs"], ["spec", "schedulingClass"], ["spec", "resourceQuotas"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "etcd"], ["spec", "monitoring"], ["spec", "ingress"], ["spec", "gateway"], ["spec", "seaweedfs"], ["spec", "schedulingClass"], ["spec", "resourceQuotas"]] secrets: exclude: [] include: [] From 8afa6601c930efa1dba2bc762d8c4e0e0ed7c809 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 17:17:59 +0300 Subject: [PATCH 05/54] fix(tenant,gateway): regenerate values.schema.json and ApplicationDefinition CRD with cozyvalues-gen v1.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit in this PR regenerated schema and CRD artefacts using a local 'dev' build of cozyvalues-gen, which sorts properties alphabetically. CI installs the pinned upstream release (v1.3.0), which preserves values.yaml source order, so the committed outputs diverged from what CI regenerates and pre-commit failed with exit code 123. Re-ran 'make generate' locally with the release binary from https://github.com/cozystack/cozyvalues-gen/releases/tag/v1.3.0 to produce the same output the CI pipeline expects. Files refreshed: - api/apps/v1alpha1/tenant/types.go — field order reverted to values.yaml source order (host, etcd, monitoring, ingress, gateway, seaweedfs, schedulingClass, resourceQuotas). - packages/apps/tenant/values.schema.json — same reordering. - packages/system/tenant-rd/cozyrds/tenant.yaml — regenerated from the refreshed schema, gateway bool field now properly sorted. - packages/extra/gateway/values.schema.json — regenerated for the same reason; gateway package itself has only one field so the diff is cosmetic, but keeps the pipeline clean. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- api/apps/v1alpha1/tenant/types.go | 38 ++++++++------- packages/apps/tenant/values.schema.json | 46 +++++++++---------- packages/extra/gateway/values.schema.json | 2 +- packages/system/tenant-rd/cozyrds/tenant.yaml | 2 +- 4 files changed, 43 insertions(+), 45 deletions(-) diff --git a/api/apps/v1alpha1/tenant/types.go b/api/apps/v1alpha1/tenant/types.go index e94d9fe7..76f865b8 100644 --- a/api/apps/v1alpha1/tenant/types.go +++ b/api/apps/v1alpha1/tenant/types.go @@ -1,10 +1,7 @@ -// +kubebuilder:object:generate=true -// +kubebuilder:object:root=true -// +groupName=values.helm.io - -// +versionName=v1alpha1 - // Code generated by values-gen. DO NOT EDIT. +// +kubebuilder:object:generate=true +// +groupName=apps.cozystack.io +// +versionName=v1alpha1 package tenant import ( @@ -12,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// +kubebuilder:object:root=true type Config struct { v1.TypeMeta `json:",inline"` v1.ObjectMeta `json:"metadata,omitempty"` @@ -19,28 +17,28 @@ type Config struct { } type ConfigSpec struct { - // Deploy own Etcd cluster. - // +kubebuilder:default:=false - Etcd bool `json:"etcd"` - // Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). - // +kubebuilder:default:=false - Gateway bool `json:"gateway"` // The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). // +kubebuilder:default:="" Host string `json:"host,omitempty"` - // Deploy own Ingress Controller. + // Deploy own Etcd cluster. // +kubebuilder:default:=false - Ingress bool `json:"ingress"` + Etcd bool `json:"etcd"` // Deploy own Monitoring Stack. // +kubebuilder:default:=false Monitoring bool `json:"monitoring"` - // Define resource quotas for the tenant. - // +kubebuilder:default:={} - ResourceQuotas map[string]resource.Quantity `json:"resourceQuotas,omitempty"` - // The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. - // +kubebuilder:default:="" - SchedulingClass string `json:"schedulingClass,omitempty"` + // Deploy own Ingress Controller. + // +kubebuilder:default:=false + Ingress bool `json:"ingress"` + // Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). + // +kubebuilder:default:=false + Gateway bool `json:"gateway"` // Deploy own SeaweedFS. // +kubebuilder:default:=false Seaweedfs bool `json:"seaweedfs"` + // The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. + // +kubebuilder:default:="" + SchedulingClass string `json:"schedulingClass,omitempty"` + // Define resource quotas for the tenant. + // +kubebuilder:default:={} + ResourceQuotas map[string]resource.Quantity `json:"resourceQuotas,omitempty"` } diff --git a/packages/apps/tenant/values.schema.json b/packages/apps/tenant/values.schema.json index f36e7b69..7a1f3df0 100644 --- a/packages/apps/tenant/values.schema.json +++ b/packages/apps/tenant/values.schema.json @@ -2,31 +2,41 @@ "title": "Chart Values", "type": "object", "properties": { + "host": { + "description": "The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).", + "type": "string", + "default": "" + }, "etcd": { "description": "Deploy own Etcd cluster.", "type": "boolean", "default": false }, + "monitoring": { + "description": "Deploy own Monitoring Stack.", + "type": "boolean", + "default": false + }, + "ingress": { + "description": "Deploy own Ingress Controller.", + "type": "boolean", + "default": false + }, "gateway": { "description": "Deploy own Gateway API Gateway (backed by Cilium Gateway API controller).", "type": "boolean", "default": false }, - "host": { - "description": "The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).", + "seaweedfs": { + "description": "Deploy own SeaweedFS.", + "type": "boolean", + "default": false + }, + "schedulingClass": { + "description": "The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.", "type": "string", "default": "" }, - "ingress": { - "description": "Deploy own Ingress Controller.", - "type": "boolean", - "default": false - }, - "monitoring": { - "description": "Deploy own Monitoring Stack.", - "type": "boolean", - "default": false - }, "resourceQuotas": { "description": "Define resource quotas for the tenant.", "type": "object", @@ -43,16 +53,6 @@ ], "x-kubernetes-int-or-string": true } - }, - "schedulingClass": { - "description": "The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.", - "type": "string", - "default": "" - }, - "seaweedfs": { - "description": "Deploy own SeaweedFS.", - "type": "boolean", - "default": false } } -} \ No newline at end of file +} diff --git a/packages/extra/gateway/values.schema.json b/packages/extra/gateway/values.schema.json index 4ec44ff6..c9f06af7 100644 --- a/packages/extra/gateway/values.schema.json +++ b/packages/extra/gateway/values.schema.json @@ -8,4 +8,4 @@ "default": "cilium" } } -} \ No newline at end of file +} diff --git a/packages/system/tenant-rd/cozyrds/tenant.yaml b/packages/system/tenant-rd/cozyrds/tenant.yaml index 765fcef5..5d352335 100644 --- a/packages/system/tenant-rd/cozyrds/tenant.yaml +++ b/packages/system/tenant-rd/cozyrds/tenant.yaml @@ -8,7 +8,7 @@ spec: singular: tenant plural: tenants openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"etcd":{"description":"Deploy own Etcd cluster.","type":"boolean","default":false},"gateway":{"description":"Deploy own Gateway API Gateway (backed by Cilium Gateway API controller).","type":"boolean","default":false},"host":{"description":"The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).","type":"string","default":""},"ingress":{"description":"Deploy own Ingress Controller.","type":"boolean","default":false},"monitoring":{"description":"Deploy own Monitoring Stack.","type":"boolean","default":false},"resourceQuotas":{"description":"Define resource quotas for the tenant.","type":"object","default":{},"additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}},"schedulingClass":{"description":"The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.","type":"string","default":""},"seaweedfs":{"description":"Deploy own SeaweedFS.","type":"boolean","default":false}}} + {"title":"Chart Values","type":"object","properties":{"host":{"description":"The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).","type":"string","default":""},"etcd":{"description":"Deploy own Etcd cluster.","type":"boolean","default":false},"monitoring":{"description":"Deploy own Monitoring Stack.","type":"boolean","default":false},"ingress":{"description":"Deploy own Ingress Controller.","type":"boolean","default":false},"gateway":{"description":"Deploy own Gateway API Gateway (backed by Cilium Gateway API controller).","type":"boolean","default":false},"seaweedfs":{"description":"Deploy own SeaweedFS.","type":"boolean","default":false},"schedulingClass":{"description":"The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.","type":"string","default":""},"resourceQuotas":{"description":"Define resource quotas for the tenant.","type":"object","default":{},"additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}}} release: prefix: tenant- labels: From 77b57e8075c8e2198cf4d6c4ac2c4930d683b850 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 18:04:56 +0300 Subject: [PATCH 06/54] feat(cert-manager): switch HTTP-01 solver to gatewayHTTPRoute when gateway.enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a cluster-wide 'gateway.enabled' flag in packages/core/platform/values.yaml (default false) and plumb it into cozystack-values as _cluster.gateway-enabled. When set to true, the cert-manager ClusterIssuers in cert-manager-issuers switch their http01 solver from the ingress-nginx backend (ingressClassName) to the Gateway API backend (gatewayHTTPRoute). Both letsencrypt-prod and letsencrypt-stage are updated in lockstep. The new solver attaches to the tenant Gateway named 'cozystack' in the publishing namespace via parentRefs with sectionName: http, so cert-manager places its ephemeral HTTPRoute on the Gateway's HTTP (:80) listener. Path matching on /.well-known/acme-challenge/ is more specific than the sibling HTTPRoute that redirects HTTP to HTTPS, so ACME challenges arrive correctly. Default behaviour (gateway.enabled=false) is unchanged — ingress-nginx path remains the only solver on existing clusters. Tests: packages/system/cert-manager-issuers/tests/solver_test.yaml adds 4 helm-unittest cases covering the ingress fallback, the gateway path for both issuers, dns01 unaffected by the flag, and the empty-string default. Picked up automatically by hack/helm-unit-tests.sh via the new Makefile test target. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/platform/templates/apps.yaml | 1 + packages/core/platform/values.yaml | 14 +++ packages/system/cert-manager-issuers/Makefile | 3 + .../templates/cluster-issuers.yaml | 110 ++++++++++-------- .../tests/solver_test.yaml | 103 ++++++++++++++++ 5 files changed, 184 insertions(+), 47 deletions(-) create mode 100644 packages/system/cert-manager-issuers/tests/solver_test.yaml diff --git a/packages/core/platform/templates/apps.yaml b/packages/core/platform/templates/apps.yaml index 6e1ae25a..fc9bbc2d 100644 --- a/packages/core/platform/templates/apps.yaml +++ b/packages/core/platform/templates/apps.yaml @@ -31,6 +31,7 @@ stringData: expose-ingress: {{ .Values.publishing.ingressName | quote }} expose-external-ips: {{ .Values.publishing.externalIPs | join "," | quote }} expose-mode: {{ .Values.publishing.exposure | default "externalIPs" | quote }} + gateway-enabled: {{ .Values.gateway.enabled | default false | quote }} cluster-domain: {{ .Values.networking.clusterDomain | quote }} api-server-endpoint: {{ .Values.publishing.apiServerEndpoint | quote }} {{- with .Values.branding }} diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 605e23bc..2e7cd931 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -94,6 +94,20 @@ authentication: # userinfo, logout) through this URL while keeping browser redirects on the external URL. # Example: http://keycloak-http.cozy-keycloak.svc:8080/realms/cozy keycloakInternalUrl: "" +# Gateway API configuration +gateway: + # Enable Gateway API support across the platform. When true: + # - cert-manager ClusterIssuers use an http01.gatewayHTTPRoute solver + # that attaches to the root tenant's Gateway, so Certificate issuance + # works for tenants whose ingress is via Gateway API rather than + # ingress-nginx. + # - Exposed services (dashboard, keycloak, harbor, bucket, grafana, + # cozystack-api, vm-exportproxy, cdi-uploadproxy) render an + # HTTPRoute attached to the tenant's Gateway. + # Per-tenant opt-in is still governed by tenant.spec.gateway=true, which + # materialises the Gateway itself. This flag only controls platform-wide + # Gateway API integration (cert-manager solver + per-service HTTPRoutes). + enabled: false # Pod scheduling configuration scheduling: globalAppTopologySpreadConstraints: "" diff --git a/packages/system/cert-manager-issuers/Makefile b/packages/system/cert-manager-issuers/Makefile index 8808dbfd..8d8245f4 100644 --- a/packages/system/cert-manager-issuers/Makefile +++ b/packages/system/cert-manager-issuers/Makefile @@ -2,3 +2,6 @@ export NAME=cert-manager-issuers export NAMESPACE=cozy-cert-manager include ../../../hack/package.mk + +test: + helm unittest . diff --git a/packages/system/cert-manager-issuers/templates/cluster-issuers.yaml b/packages/system/cert-manager-issuers/templates/cluster-issuers.yaml index 3b582082..3414dbbc 100644 --- a/packages/system/cert-manager-issuers/templates/cluster-issuers.yaml +++ b/packages/system/cert-manager-issuers/templates/cluster-issuers.yaml @@ -1,57 +1,73 @@ {{- $solver := (index .Values._cluster "solver") | default "http01" }} {{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} + +{{- define "cert-manager-issuers.httpSolver" -}} +{{- $gatewayEnabled := index . "gatewayEnabled" -}} +{{- $exposeIngress := index . "exposeIngress" -}} +{{- if eq $gatewayEnabled "true" }} +http01: + gatewayHTTPRoute: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $exposeIngress }} + sectionName: http +{{- else }} +http01: + ingress: + ingressClassName: {{ $exposeIngress }} +{{- end }} +{{- end }} apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: letsencrypt-prod -spec: - acme: - privateKeySecretRef: - name: letsencrypt-prod - server: https://acme-v02.api.letsencrypt.org/directory - solvers: - - {{- if eq $solver "dns01" }} - dns01: - cloudflare: - apiTokenSecretRef: - name: cloudflare-api-token-secret - key: api-token - {{- else }} - http01: - ingress: - ingressClassName: {{ $exposeIngress }} - {{- end }} +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + privateKeySecretRef: + name: letsencrypt-prod + server: https://acme-v02.api.letsencrypt.org/directory + solvers: + - {{- if eq $solver "dns01" }} + dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token-secret + key: api-token + {{- else }} + {{- include "cert-manager-issuers.httpSolver" (dict "gatewayEnabled" $gatewayEnabled "exposeIngress" $exposeIngress) | nindent 8 }} + {{- end }} ---- +--- -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: letsencrypt-stage -spec: - acme: - privateKeySecretRef: +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-stage +spec: + acme: + privateKeySecretRef: name: letsencrypt-stage - server: https://acme-staging-v02.api.letsencrypt.org/directory - solvers: - - {{- if eq $solver "dns01" }} - dns01: - cloudflare: - apiTokenSecretRef: - name: cloudflare-api-token-secret - key: api-token - {{- else }} - http01: - ingress: - ingressClassName: {{ $exposeIngress }} - {{- end }} + server: https://acme-staging-v02.api.letsencrypt.org/directory + solvers: + - {{- if eq $solver "dns01" }} + dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token-secret + key: api-token + {{- else }} + {{- include "cert-manager-issuers.httpSolver" (dict "gatewayEnabled" $gatewayEnabled "exposeIngress" $exposeIngress) | nindent 8 }} + {{- end }} ---- +--- -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: selfsigned-cluster-issuer -spec: +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-cluster-issuer +spec: selfSigned: {} diff --git a/packages/system/cert-manager-issuers/tests/solver_test.yaml b/packages/system/cert-manager-issuers/tests/solver_test.yaml new file mode 100644 index 00000000..67f76b80 --- /dev/null +++ b/packages/system/cert-manager-issuers/tests/solver_test.yaml @@ -0,0 +1,103 @@ +suite: cluster-issuer solver selection +templates: + - templates/cluster-issuers.yaml + +release: + name: cert-manager-issuers + namespace: cozy-cert-manager + +tests: + - it: default renders ingress-nginx HTTP-01 solver on both ACME issuers + set: + _cluster: + solver: http01 + expose-ingress: tenant-root + asserts: + - documentIndex: 0 + equal: + path: metadata.name + value: letsencrypt-prod + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.ingress.ingressClassName + value: tenant-root + - documentIndex: 0 + notExists: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute + - documentIndex: 1 + equal: + path: metadata.name + value: letsencrypt-stage + - documentIndex: 1 + equal: + path: spec.acme.solvers[0].http01.ingress.ingressClassName + value: tenant-root + + - it: gateway-enabled=true renders gatewayHTTPRoute solver on both ACME issuers + set: + _cluster: + solver: http01 + expose-ingress: tenant-root + gateway-enabled: "true" + asserts: + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].group + value: gateway.networking.k8s.io + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].kind + value: Gateway + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].name + value: cozystack + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].namespace + value: tenant-root + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].sectionName + value: http + - documentIndex: 0 + notExists: + path: spec.acme.solvers[0].http01.ingress + - documentIndex: 1 + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].name + value: cozystack + + - it: solver=dns01 is unaffected by gateway-enabled flag + set: + _cluster: + solver: dns01 + expose-ingress: tenant-root + gateway-enabled: "true" + asserts: + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].dns01.cloudflare.apiTokenSecretRef.name + value: cloudflare-api-token-secret + - documentIndex: 0 + notExists: + path: spec.acme.solvers[0].http01 + - documentIndex: 1 + equal: + path: spec.acme.solvers[0].dns01.cloudflare.apiTokenSecretRef.name + value: cloudflare-api-token-secret + + - it: gateway-enabled defaults to ingress solver when unset + set: + _cluster: + solver: http01 + expose-ingress: tenant-root + gateway-enabled: "" + asserts: + - documentIndex: 0 + equal: + path: spec.acme.solvers[0].http01.ingress.ingressClassName + value: tenant-root + - documentIndex: 0 + notExists: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute From 6c2872af33728c4dd82f5c7e33d8a3053f7c1054 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 18:13:24 +0300 Subject: [PATCH 07/54] fix(gateway): restrict tenant Gateway listener allowedRoutes to an explicit namespace whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fix: before this change the Gateway listeners used 'allowedRoutes.namespaces.from: Selector' with the tenant namespace label, but when per-service HTTPRoutes had to live outside the tenant namespace that label model broke — so the natural fix was to relax to 'from: All'. That would have let any namespace in the cluster attach an HTTPRoute with an arbitrary hostname to the publishing tenant's Gateway, which is hostname hijacking across tenants. The correct solution is to pin each listener to an explicit namespace whitelist using the built-in 'kubernetes.io/metadata.name' label that kube-apiserver assigns to every namespace by default. Namespaces not on the whitelist literally cannot attach HTTPRoutes to this Gateway, so a non-publishing tenant cannot create a hijacking HTTPRoute in its own namespace. The whitelist is the union of: - The publishing tenant namespace (always, implicitly added). - 'publishing.gateway.attachedNamespaces' in the platform chart — a list of system namespaces that host exposed services and need to attach HTTPRoutes (cozy-cert-manager for ACME, cozy-dashboard, cozy-keycloak, cozy-system, cozy-harbor, cozy-bucket, cozy-kubevirt, cozy-kubevirt-cdi, cozy-monitoring, cozy-linstor-gui). Plumbed through cozystack-values as '_cluster.gateway-attached-namespaces' (CSV of namespaces). The gateway chart concats this CSV with the release namespace, de-dupes via 'uniq', and emits the result as the matchExpressions 'values' list on every listener. Tests: 3 new helm-unittest cases in packages/extra/gateway/tests/ covering the default (publishing namespace only), CSV parsing with empty-entry filtering, and the invariant that HTTPS listeners share the whitelist with the HTTP listener. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/platform/templates/apps.yaml | 1 + packages/core/platform/values.yaml | 17 +++++ packages/extra/gateway/templates/gateway.yaml | 21 ++++-- .../extra/gateway/tests/gateway_test.yaml | 67 +++++++++++++++++++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/packages/core/platform/templates/apps.yaml b/packages/core/platform/templates/apps.yaml index fc9bbc2d..fa4db7bc 100644 --- a/packages/core/platform/templates/apps.yaml +++ b/packages/core/platform/templates/apps.yaml @@ -32,6 +32,7 @@ stringData: expose-external-ips: {{ .Values.publishing.externalIPs | join "," | quote }} expose-mode: {{ .Values.publishing.exposure | default "externalIPs" | quote }} gateway-enabled: {{ .Values.gateway.enabled | default false | quote }} + gateway-attached-namespaces: {{ .Values.gateway.attachedNamespaces | default (list) | join "," | quote }} cluster-domain: {{ .Values.networking.clusterDomain | quote }} api-server-endpoint: {{ .Values.publishing.apiServerEndpoint | quote }} {{- with .Values.branding }} diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 2e7cd931..c740e656 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -108,6 +108,23 @@ gateway: # materialises the Gateway itself. This flag only controls platform-wide # Gateway API integration (cert-manager solver + per-service HTTPRoutes). enabled: false + # Namespaces that are allowed to attach HTTPRoutes to a tenant Gateway. + # The publishing tenant namespace is always included implicitly. Additional + # entries must be the exact namespace name (matched on the built-in + # kubernetes.io/metadata.name label). Tenants not on this list cannot + # attach an HTTPRoute to somebody else's Gateway, which prevents hostname + # hijacking across tenants. + attachedNamespaces: + - cozy-cert-manager + - cozy-dashboard + - cozy-keycloak + - cozy-system + - cozy-harbor + - cozy-bucket + - cozy-kubevirt + - cozy-kubevirt-cdi + - cozy-monitoring + - cozy-linstor-gui # Pod scheduling configuration scheduling: globalAppTopologySpreadConstraints: "" diff --git a/packages/extra/gateway/templates/gateway.yaml b/packages/extra/gateway/templates/gateway.yaml index c3fb0ee3..38fe612b 100644 --- a/packages/extra/gateway/templates/gateway.yaml +++ b/packages/extra/gateway/templates/gateway.yaml @@ -30,6 +30,18 @@ metadata: spec: gatewayClassName: {{ .Values.gatewayClassName | quote }} listeners: + {{- $extraNs := list }} + {{- range splitList "," ((index .Values._cluster "gateway-attached-namespaces") | default "") }} + {{- $ns := . | trim }} + {{- if $ns }} + {{- $extraNs = append $extraNs $ns }} + {{- end }} + {{- end }} + {{- $allowedNamespaces := concat (list .Release.Namespace) $extraNs | uniq }} + {{- $allowedNamespacesSelector := dict "matchExpressions" (list (dict + "key" "kubernetes.io/metadata.name" + "operator" "In" + "values" $allowedNamespaces)) }} - name: http protocol: HTTP port: 80 @@ -37,8 +49,7 @@ spec: namespaces: from: Selector selector: - matchLabels: - cozystack.io/gateway: {{ .Release.Namespace }} + {{- toYaml $allowedNamespacesSelector | nindent 10 }} - name: https protocol: HTTPS port: 443 @@ -51,8 +62,7 @@ spec: namespaces: from: Selector selector: - matchLabels: - cozystack.io/gateway: {{ .Release.Namespace }} + {{- toYaml $allowedNamespacesSelector | nindent 10 }} - name: https-apex protocol: HTTPS port: 443 @@ -65,8 +75,7 @@ spec: namespaces: from: Selector selector: - matchLabels: - cozystack.io/gateway: {{ .Release.Namespace }} + {{- toYaml $allowedNamespacesSelector | nindent 10 }} --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml index 7e1cfe0b..b74d7e6d 100644 --- a/packages/extra/gateway/tests/gateway_test.yaml +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -141,3 +141,70 @@ tests: - template: templates/gateway.yaml failedTemplate: errorMessage: "publishing.host must be set in the platform chart for packages/extra/gateway to render a wildcard TLS listener." + + - it: allowedRoutes whitelist defaults to publishing namespace only when no extra namespaces are set + set: + _cluster: + expose-ingress: tenant-root + host: example.org + asserts: + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].allowedRoutes.namespaces.from + value: Selector + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].allowedRoutes.namespaces.selector.matchExpressions[0].key + value: kubernetes.io/metadata.name + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].allowedRoutes.namespaces.selector.matchExpressions[0].operator + value: In + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].allowedRoutes.namespaces.selector.matchExpressions[0].values + value: + - tenant-root + + - it: allowedRoutes whitelist prepends publishing namespace to the gateway-attached-namespaces list and filters empty entries + set: + _cluster: + expose-ingress: tenant-root + host: example.org + gateway-attached-namespaces: "cozy-cert-manager,,cozy-dashboard, cozy-keycloak," + asserts: + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].allowedRoutes.namespaces.selector.matchExpressions[0].values + value: + - tenant-root + - cozy-cert-manager + - cozy-dashboard + - cozy-keycloak + + - it: HTTPS and HTTPS-apex listeners share the same allowedRoutes whitelist + set: + _cluster: + expose-ingress: tenant-root + host: example.org + gateway-attached-namespaces: "cozy-dashboard" + asserts: + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[1].allowedRoutes.namespaces.selector.matchExpressions[0].values + value: + - tenant-root + - cozy-dashboard + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[2].allowedRoutes.namespaces.selector.matchExpressions[0].values + value: + - tenant-root + - cozy-dashboard From 142108e5075e90a5bd9b2974c33d683087b61cbb Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 18:17:55 +0300 Subject: [PATCH 08/54] feat(dashboard,keycloak): attach to tenant Gateway via HTTPRoute when gateway.enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each package now ships a conditional templates/httproute.yaml which is rendered only when the cluster-wide gateway.enabled flag is true (propagated via _cluster.gateway-enabled). The existing Ingress template is wrapped in the inverse condition, so exactly one of the two is rendered at a time and traffic does not split. Dashboard: HTTPRoute hostname dashboard., backendRef incloud-web-gatekeeper:8000 (same as the existing Ingress). Keycloak: HTTPRoute hostname (default keycloak.), backendRef keycloak-http:80. Both routes parentRef the tenant Gateway named 'cozystack' in the publishing namespace and rely on the namespace whitelist on Gateway listeners (see 'fix(gateway): restrict tenant Gateway listener allowedRoutes') to ensure only authorised namespaces can attach. Known gap (follow-up): services that rely on TLS passthrough (cozystack-api, vm-exportproxy, cdi-uploadproxy) still render their existing Ingress regardless of gateway.enabled, because Gateway API passthrough requires a separate TLSRoute + a dedicated mode: Passthrough listener on the Gateway. Harbor and bucket are tenant-scoped apps that need _namespace.gateway plumbing — also follow-up. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../system/dashboard/templates/httproute.yaml | 22 +++++++++++++++++++ .../system/dashboard/templates/ingress.yaml | 3 ++- .../system/keycloak/templates/httproute.yaml | 22 +++++++++++++++++++ .../system/keycloak/templates/ingress.yaml | 3 +++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/system/dashboard/templates/httproute.yaml create mode 100644 packages/system/keycloak/templates/httproute.yaml diff --git a/packages/system/dashboard/templates/httproute.yaml b/packages/system/dashboard/templates/httproute.yaml new file mode 100644 index 00000000..4a6cc4c9 --- /dev/null +++ b/packages/system/dashboard/templates/httproute.yaml @@ -0,0 +1,22 @@ +{{- $host := index .Values._cluster "root-host" }} +{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- if and (has "dashboard" $exposeServices) (eq $gatewayEnabled "true") }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $exposeIngress }} + hostnames: + - {{ printf "dashboard.%s" $host | quote }} + rules: + - backendRefs: + - name: incloud-web-gatekeeper + port: 8000 +{{- end }} diff --git a/packages/system/dashboard/templates/ingress.yaml b/packages/system/dashboard/templates/ingress.yaml index 6cf01490..f69e7e4e 100644 --- a/packages/system/dashboard/templates/ingress.yaml +++ b/packages/system/dashboard/templates/ingress.yaml @@ -3,8 +3,9 @@ {{- $host := index .Values._cluster "root-host" }} {{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} {{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} -{{- if and (has "dashboard" $exposeServices) }} +{{- if and (has "dashboard" $exposeServices) (ne $gatewayEnabled "true") }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/packages/system/keycloak/templates/httproute.yaml b/packages/system/keycloak/templates/httproute.yaml new file mode 100644 index 00000000..d6d9bb58 --- /dev/null +++ b/packages/system/keycloak/templates/httproute.yaml @@ -0,0 +1,22 @@ +{{- $host := index .Values._cluster "root-host" }} +{{- $ingressHost := .Values.ingress.host | default (printf "keycloak.%s" $host) }} +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- if eq $gatewayEnabled "true" }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: keycloak +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $exposeIngress }} + hostnames: + - {{ $ingressHost | quote }} + rules: + - backendRefs: + - name: keycloak-http + port: 80 +{{- end }} diff --git a/packages/system/keycloak/templates/ingress.yaml b/packages/system/keycloak/templates/ingress.yaml index 7f9fd476..07000f51 100644 --- a/packages/system/keycloak/templates/ingress.yaml +++ b/packages/system/keycloak/templates/ingress.yaml @@ -3,6 +3,8 @@ {{- $solver := (index .Values._cluster "solver") | default "http01" }} {{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} {{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- if ne $gatewayEnabled "true" }} apiVersion: networking.k8s.io/v1 kind: Ingress @@ -33,3 +35,4 @@ spec: name: keycloak-http port: name: http +{{- end }} From bb3fc7cf261279b442b6509e90bd5ed1b1f598f5 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 18:19:54 +0300 Subject: [PATCH 09/54] feat(cozystack-basics): ValidatingAdmissionPolicy blocks cross-tenant hostname hijacking on Gateways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When gateway.enabled is true and publishing.host is set, cozystack-basics now installs a ValidatingAdmissionPolicy (plus its Binding and a ConfigMap that carries the apex host and publishing namespace as parameters) that rejects any Gateway whose listeners reference a hostname outside the tenant's own domain suffix. Scheme: - Publishing tenant namespace ($publishing.ingressName, default tenant-root): allowed suffix is the platform-wide $publishing.host. - Any namespace prefixed with 'tenant-': allowed suffix is .$publishing.host. - Any other namespace: the CEL variable resolves to "" and every listener with a hostname is rejected — only cluster-admin-managed namespaces should be creating Gateways anyway. The VAP is scoped to CREATE and UPDATE on gateway.networking.k8s.io/v1 (and v1beta1) Gateways cluster-wide; the Binding uses validationActions: [Deny] with parameterNotFoundAction: Deny so that if the ConfigMap is missing the policy fails safe instead of open-allowing. messageExpression spells out the tenant-scoped allowed suffix in the rejection, so operators can see at a glance why the request was denied. This closes the hostname-hijacking half of the multi-tenant threat model on top of the namespace whitelist on Gateway listeners. The two work in layers: the whitelist stops a non-authorised namespace from attaching any HTTPRoute; the VAP stops an authorised namespace from claiming someone else's hostname on its own Gateway. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml new file mode 100644 index 00000000..f45c5067 --- /dev/null +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -0,0 +1,74 @@ +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- $apexHost := (index .Values._cluster "root-host") | default "" }} +{{- $publishingNamespace := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- if and (eq $gatewayEnabled "true") $apexHost }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cozystack-gateway-hostname-policy + namespace: cozy-system + labels: + internal.cozystack.io/managed-by-cozystack: "" +data: + apexHost: {{ $apexHost | quote }} + publishingNamespace: {{ $publishingNamespace | quote }} +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: cozystack-gateway-hostname-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["gateway.networking.k8s.io"] + apiVersions: ["v1", "v1beta1"] + operations: ["CREATE", "UPDATE"] + resources: ["gateways"] + paramKind: + apiVersion: v1 + kind: ConfigMap + variables: + - name: apexHost + expression: "params.data.apexHost" + - name: publishingNamespace + expression: "params.data.publishingNamespace" + - name: tenantSuffix + expression: >- + object.metadata.namespace == variables.publishingNamespace + ? variables.apexHost + : (object.metadata.namespace.startsWith("tenant-") + ? object.metadata.namespace.substring(7) + "." + variables.apexHost + : "") + validations: + - expression: >- + variables.tenantSuffix != "" && + (!has(object.spec.listeners) || + object.spec.listeners.all(l, + !has(l.hostname) || + l.hostname == variables.tenantSuffix || + l.hostname.endsWith("." + variables.tenantSuffix) + )) + messageExpression: >- + "Gateway listener hostname must equal " + variables.tenantSuffix + + " or end with ." + variables.tenantSuffix + + " (namespace " + object.metadata.namespace + " may only publish its own tenant's domain)" + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: cozystack-gateway-hostname-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + policyName: cozystack-gateway-hostname-policy + paramRef: + name: cozystack-gateway-hostname-policy + namespace: cozy-system + parameterNotFoundAction: Deny + validationActions: [Deny] +{{- end }} From 9505b08dd24e28dd51f919e496393b4670c566b7 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 18:22:43 +0300 Subject: [PATCH 10/54] test(e2e): gateway.bats smoke-tests Cilium Gateway API wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bats test cases verify the end-to-end infrastructure brought up by this PR: 1. Gateway API CRDs are installed (gatewayclasses, gateways, httproutes) and the 'cilium' GatewayClass is Accepted by the controller — proves gateway-api-crds installed and cilium.gatewayAPI.enabled propagated. 2. A minimal Gateway in tenant-test reconciles to Programmed and the controller materialises its cilium-gateway- LoadBalancer Service — proves envoy.enabled kicked in and the data-plane wiring is live. 3. An HTTPRoute with a matching parentRef reaches Accepted status — proves Cilium's HTTPRoute attachment logic works. Not covered (because the test harness has no reachable Let's Encrypt endpoint and no way to flip cluster-wide gateway.enabled mid-pipeline without trampling other tests): - cert-manager HTTP-01 solver via gatewayHTTPRoute. - The ValidatingAdmissionPolicy that enforces tenant hostname ownership (installed only when gateway.enabled=true at platform level). - Full tenant.spec.gateway=true flow with extra/gateway chart and CiliumLoadBalancerIPPool. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 91 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 hack/e2e-apps/gateway.bats diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats new file mode 100644 index 00000000..211f9bf1 --- /dev/null +++ b/hack/e2e-apps/gateway.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats + +@test "Gateway API CRDs are installed and the cilium GatewayClass is Accepted" { + # Gateway API CRDs must exist — installed by packages/system/gateway-api-crds + kubectl wait crd/gatewayclasses.gateway.networking.k8s.io --for=condition=Established --timeout=60s + kubectl wait crd/gateways.gateway.networking.k8s.io --for=condition=Established --timeout=60s + kubectl wait crd/httproutes.gateway.networking.k8s.io --for=condition=Established --timeout=60s + + # Cilium must have registered its built-in GatewayClass once gatewayAPI.enabled + # is true in the cilium values. This verifies the flip in + # packages/system/cilium/values.yaml propagated end-to-end. + timeout 120 sh -ec 'until kubectl get gatewayclass cilium >/dev/null 2>&1; do sleep 2; done' + kubectl wait gatewayclass/cilium --for=condition=Accepted --timeout=3m +} + +@test "Cilium Gateway API controller reconciles a minimal Gateway to Programmed" { + # Use the pre-existing tenant-test namespace created by e2e-install-cozystack.bats. + kubectl apply -f - <<'EOF' +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-e2e-probe + namespace: tenant-test +spec: + gatewayClassName: cilium + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same +EOF + + # The controller must accept and program the Gateway. + kubectl -n tenant-test wait gateway/gateway-e2e-probe --for=condition=Accepted --timeout=2m + kubectl -n tenant-test wait gateway/gateway-e2e-probe --for=condition=Programmed --timeout=3m + + # Cilium materialises a LoadBalancer Service named cilium-gateway- + # for each programmed Gateway. Its existence is the observable proof that the + # full data-plane wiring kicked in. + kubectl -n tenant-test get svc cilium-gateway-gateway-e2e-probe + + # Cleanup + kubectl -n tenant-test delete gateway/gateway-e2e-probe --ignore-not-found --timeout=1m +} + +@test "HTTPRoute with a matching parentRef reaches Accepted status" { + # Put a Gateway and a route in the same namespace so allowedRoutes: Same accepts them. + kubectl apply -f - <<'EOF' +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-route-probe + namespace: tenant-test +spec: + gatewayClassName: cilium + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httproute-probe + namespace: tenant-test +spec: + parentRefs: + - name: gateway-route-probe + sectionName: http + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: kubernetes + namespace: default + port: 443 +EOF + + kubectl -n tenant-test wait gateway/gateway-route-probe --for=condition=Programmed --timeout=3m + timeout 120 sh -ec 'until kubectl -n tenant-test get httproute httproute-probe -o jsonpath="{.status.parents[0].conditions[?(@.type==\"Accepted\")].status}" 2>/dev/null | grep -q True; do sleep 2; done' + + kubectl -n tenant-test delete httproute/httproute-probe --ignore-not-found + kubectl -n tenant-test delete gateway/gateway-route-probe --ignore-not-found +} From 6914b8dbc0df689765b7ce5553e849e0273e69a8 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 18:23:58 +0300 Subject: [PATCH 11/54] docs(gateway): document security model, Let's Encrypt rate limits, known gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add operator-facing documentation for the Gateway API feature: packages/extra/gateway/README.md grows three sections: - Security model — explains the two layers that protect cross-tenant isolation (Gateway listener allowedRoutes namespace whitelist + ValidatingAdmissionPolicy for tenant hostname ownership). Makes it explicit which namespaces are on the default whitelist so operators who add a new system component know where to register it. - Rate limits — spells out the Let's Encrypt quotas (50 certs / registered domain / week, 5 duplicate certs / week, 300 new orders / account / 3h), lists mitigations (letsencrypt-stage, resourceQuotas with count/certificates.cert-manager.io, self-signed ClusterIssuer, internal ACME). - Known limitations — TLS passthrough services still use ingress-nginx, tenant-scoped apps (harbor, bucket) not yet wired to per-tenant Gateway, child-tenant ACME HTTP-01 needs a namespace-scoped Issuer. All three are tracked as follow-up work rather than hidden failure modes. packages/core/platform/values.yaml adds a comment block under publishing.certificates.issuerName reminding operators of the rate-limit consequences before they flip gateway.enabled=true on a production cluster. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/platform/values.yaml | 9 +++++++++ packages/extra/gateway/README.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index c740e656..1a675ae7 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -83,6 +83,15 @@ publishing: certificates: solver: http01 # "http01" or "dns01" issuerName: letsencrypt-prod + # Rate-limit note: every tenant with gateway=true issues one + # Certificate for its wildcard + apex hostnames. If many tenants share + # the same apex domain with issuerName=letsencrypt-prod, the cluster + # can hit the Let's Encrypt limits (50 certs / registered domain / + # week, 5 duplicate certs / week, 300 new orders / account / 3h). + # Use letsencrypt-stage for non-production clusters or gate the + # number of certificates per tenant via tenant.spec.resourceQuotas + # with count/certificates.cert-manager.io. See + # https://letsencrypt.org/docs/rate-limits/. # Authentication configuration authentication: oidc: diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index cec54018..c74b90ab 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -10,3 +10,31 @@ Per-tenant Gateway API Gateway backed by Cilium. Installed automatically when `t | ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | -------- | | `gatewayClassName` | GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class. | `string` | `cilium` | + +## Security model + +Two layers protect cross-tenant isolation: + +1. **Namespace whitelist on listeners.** The Gateway's `allowedRoutes.namespaces.from: Selector` only accepts HTTPRoutes from the publishing tenant's namespace and from `publishing.gateway.attachedNamespaces` in the platform chart (default: cozy-cert-manager, cozy-dashboard, cozy-keycloak, cozy-system, cozy-harbor, cozy-bucket, cozy-kubevirt, cozy-kubevirt-cdi, cozy-monitoring, cozy-linstor-gui). A tenant namespace that is not on this list cannot attach an HTTPRoute to this Gateway at all. +2. **Hostname ownership via ValidatingAdmissionPolicy.** `cozystack-gateway-hostname-policy` (installed by cozystack-basics when `gateway.enabled` is true) rejects any Gateway whose listener hostnames fall outside the tenant's own domain suffix. For `tenant-root` the allowed suffix is `publishing.host`; for any `tenant-` it is `.publishing.host`. Other namespaces are rejected outright. + +## Rate limits + +cert-manager issues one `Certificate` per Gateway release. With `issuerName: letsencrypt-prod` (the default), the `Certificate` for a tenant Gateway counts against the [Let's Encrypt rate limits](https://letsencrypt.org/docs/rate-limits/): + +- 50 new certificates per registered domain per week. +- 5 duplicate certificates per week for the same set of hostnames. +- 300 new orders per account per 3 hours. + +A cluster where many tenants share the same apex domain can exhaust these quickly. Mitigations: + +- Use `publishing.certificates.issuerName: letsencrypt-stage` for non-production clusters (staging does not count against prod quotas). +- Limit the number of simultaneous tenant Gateways per cluster via the platform's package quota, or cap it via `tenant.spec.resourceQuotas` with `count/certificates.cert-manager.io` to limit how many `Certificate` objects a tenant may create. +- For bare-metal or air-gapped deployments consider an internal ACME server or the self-signed `ClusterIssuer` (`selfsigned-cluster-issuer`) that ships alongside the Let's Encrypt issuers. + +## Known limitations + +- **TLS passthrough services** (`cozystack-api`, `vm-exportproxy`, `cdi-uploadproxy`) are not migrated to the Gateway. They keep rendering their existing Ingress regardless of `gateway.enabled`. A follow-up PR will add a Passthrough listener + `TLSRoute` per passthrough service. +- **Tenant-scoped apps** (`harbor`, `bucket`) are not yet wired to a tenant's own Gateway — they still use ingress-nginx even when `gateway.enabled=true`. Follow-up work needs to plumb `gateway` through `_namespace` in `packages/apps/tenant/templates/namespace.yaml` so the apps know which Gateway to attach to. +- **Child-tenant ACME HTTP-01** currently relies on the publishing tenant's Gateway; a child tenant that turns on `gateway: true` but still has its issuer pointed at `letsencrypt-prod` (HTTP-01) must either share the parent Gateway or switch to `dns01`. A proper fix is a namespace-scoped `Issuer` per tenant — tracked as a follow-up. + From 2c5bf50f43b0da241cc520b112aa68e7c6b9ba71 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 19:54:12 +0300 Subject: [PATCH 12/54] feat(gateway): TLSRoute for TLS-passthrough services (cozystack-api, vm-exportproxy, cdi-uploadproxy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the three HTTPS-passthrough services through Gateway API so ingress-nginx is no longer required for them when gateway.enabled=true. extra/gateway chart: new values.tlsPassthroughServices (defaults to [api, vm-exportproxy, cdi-uploadproxy]). For each service in that list AND in _cluster.expose-services, the Gateway emits an extra port-443 listener with protocol: TLS, tls.mode: Passthrough, hostname ., and allowedRoutes.kinds restricted to TLSRoute. The specific hostname wins over the wildcard HTTPS Terminate listener at SNI-matching time per Gateway API spec. Per-service packages: - packages/system/cozystack-api: existing Ingress wrapped in '!= gateway-enabled=true'. New api-tlsroute.yaml renders apiVersion gateway.networking.k8s.io/v1alpha2 TLSRoute in the 'default' namespace (where the kubernetes Service lives) targeting the tls-api listener, hostname api., backendRef kubernetes:443. - packages/system/kubevirt: same treatment for vm-exportproxy-ingress.yaml → vm-exportproxy-tlsroute.yaml, hostname vm-exportproxy., backendRef vm-exportproxy:443 in cozy-kubevirt. - packages/system/kubevirt-cdi: same for cdi-uploadproxy — hostname cdi-uploadproxy., backendRef cdi-uploadproxy:443 in cozy-kubevirt-cdi. TLSRoute apiVersion v1alpha2 is intentional: Cilium 1.19 vendors Gateway API CRDs v1.4 (TLSRoute is v1alpha2 there) and v1.5.1 ships both v1 and v1alpha2 in the experimental channel, so v1alpha2 is compatible with both. A follow-up can bump to v1 once Cilium v1.20 (Gateway API v1.5) lands in the platform. Backward compatibility: default gateway.enabled=false keeps the existing Ingress path verbatim for all three services. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/extra/gateway/README.md | 7 +++--- packages/extra/gateway/templates/gateway.yaml | 20 ++++++++++++++++ packages/extra/gateway/values.schema.json | 12 ++++++++++ packages/extra/gateway/values.yaml | 6 +++++ .../cozystack-api/templates/api-ingress.yaml | 3 ++- .../cozystack-api/templates/api-tlsroute.yaml | 24 +++++++++++++++++++ .../templates/cdi-uploadproxy-ingress.yaml | 3 ++- .../templates/cdi-uploadproxy-tlsroute.yaml | 23 ++++++++++++++++++ .../templates/vm-exportproxy-ingress.yaml | 3 ++- .../templates/vm-exportproxy-tlsroute.yaml | 23 ++++++++++++++++++ 10 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 packages/system/cozystack-api/templates/api-tlsroute.yaml create mode 100644 packages/system/kubevirt-cdi/templates/cdi-uploadproxy-tlsroute.yaml create mode 100644 packages/system/kubevirt/templates/vm-exportproxy-tlsroute.yaml diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index c74b90ab..673cbaa8 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -6,9 +6,10 @@ Per-tenant Gateway API Gateway backed by Cilium. Installed automatically when `t ### Common parameters -| Name | Description | Type | Value | -| ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | -------- | -| `gatewayClassName` | GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class. | `string` | `cilium` | +| Name | Description | Type | Value | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ---------------------------------------- | +| `gatewayClassName` | GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class. | `string` | `cilium` | +| `tlsPassthroughServices` | Names (from publishing.exposedServices) whose traffic is TLS-passthrough rather than TLS-terminate. For each such service a dedicated HTTPS listener with tls.mode=Passthrough is rendered on the Gateway, and the service is expected to attach a TLSRoute instead of an HTTPRoute. | `[]string` | `[api, vm-exportproxy, cdi-uploadproxy]` | ## Security model diff --git a/packages/extra/gateway/templates/gateway.yaml b/packages/extra/gateway/templates/gateway.yaml index 38fe612b..fb33546e 100644 --- a/packages/extra/gateway/templates/gateway.yaml +++ b/packages/extra/gateway/templates/gateway.yaml @@ -76,6 +76,26 @@ spec: from: Selector selector: {{- toYaml $allowedNamespacesSelector | nindent 10 }} + {{- $exposedServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} + {{- range .Values.tlsPassthroughServices }} + {{- $svc := . }} + {{- if has $svc $exposedServices }} + - name: {{ printf "tls-%s" $svc | quote }} + protocol: TLS + port: 443 + hostname: {{ printf "%s.%s" $svc $host | quote }} + tls: + mode: Passthrough + allowedRoutes: + kinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + namespaces: + from: Selector + selector: + {{- toYaml $allowedNamespacesSelector | nindent 10 }} + {{- end }} + {{- end }} --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute diff --git a/packages/extra/gateway/values.schema.json b/packages/extra/gateway/values.schema.json index c9f06af7..6a093ebb 100644 --- a/packages/extra/gateway/values.schema.json +++ b/packages/extra/gateway/values.schema.json @@ -6,6 +6,18 @@ "description": "GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class.", "type": "string", "default": "cilium" + }, + "tlsPassthroughServices": { + "description": "Names (from publishing.exposedServices) whose traffic is TLS-passthrough rather than TLS-terminate. For each such service a dedicated HTTPS listener with tls.mode=Passthrough is rendered on the Gateway, and the service is expected to attach a TLSRoute instead of an HTTPRoute.", + "type": "array", + "default": [ + "api", + "vm-exportproxy", + "cdi-uploadproxy" + ], + "items": { + "type": "string" + } } } } diff --git a/packages/extra/gateway/values.yaml b/packages/extra/gateway/values.yaml index 7831f7f9..74d82248 100644 --- a/packages/extra/gateway/values.yaml +++ b/packages/extra/gateway/values.yaml @@ -4,3 +4,9 @@ ## @param {string} gatewayClassName - GatewayClass to attach the tenant Gateway to. Must exist cluster-wide. Default matches the Cilium-managed class. gatewayClassName: cilium + +## @param {[]string} tlsPassthroughServices - Names (from publishing.exposedServices) whose traffic is TLS-passthrough rather than TLS-terminate. For each such service a dedicated HTTPS listener with tls.mode=Passthrough is rendered on the Gateway, and the service is expected to attach a TLSRoute instead of an HTTPRoute. +tlsPassthroughServices: + - api + - vm-exportproxy + - cdi-uploadproxy diff --git a/packages/system/cozystack-api/templates/api-ingress.yaml b/packages/system/cozystack-api/templates/api-ingress.yaml index 9226d887..454f2367 100644 --- a/packages/system/cozystack-api/templates/api-ingress.yaml +++ b/packages/system/cozystack-api/templates/api-ingress.yaml @@ -1,8 +1,9 @@ {{- $host := index .Values._cluster "root-host" }} {{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} {{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} -{{- if and (has "api" $exposeServices) }} +{{- if and (has "api" $exposeServices) (ne $gatewayEnabled "true") }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/packages/system/cozystack-api/templates/api-tlsroute.yaml b/packages/system/cozystack-api/templates/api-tlsroute.yaml new file mode 100644 index 00000000..10ce9e16 --- /dev/null +++ b/packages/system/cozystack-api/templates/api-tlsroute.yaml @@ -0,0 +1,24 @@ +{{- $host := index .Values._cluster "root-host" }} +{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- if and (has "api" $exposeServices) (eq $gatewayEnabled "true") }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: kubernetes-api + namespace: default +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $exposeIngress }} + sectionName: tls-api + hostnames: + - {{ printf "api.%s" $host | quote }} + rules: + - backendRefs: + - name: kubernetes + port: 443 +{{- end }} diff --git a/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-ingress.yaml b/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-ingress.yaml index ee89953f..dcfc44bb 100644 --- a/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-ingress.yaml +++ b/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-ingress.yaml @@ -1,9 +1,10 @@ {{- $host := index .Values._cluster "root-host" }} {{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} {{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} -{{- if and (has "cdi-uploadproxy" $exposeServices) }} +{{- if and (has "cdi-uploadproxy" $exposeServices) (ne $gatewayEnabled "true") }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-tlsroute.yaml b/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-tlsroute.yaml new file mode 100644 index 00000000..0f76ac0f --- /dev/null +++ b/packages/system/kubevirt-cdi/templates/cdi-uploadproxy-tlsroute.yaml @@ -0,0 +1,23 @@ +{{- $host := index .Values._cluster "root-host" }} +{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- if and (has "cdi-uploadproxy" $exposeServices) (eq $gatewayEnabled "true") }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: cdi-uploadproxy +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $exposeIngress }} + sectionName: tls-cdi-uploadproxy + hostnames: + - {{ printf "cdi-uploadproxy.%s" $host | quote }} + rules: + - backendRefs: + - name: cdi-uploadproxy + port: 443 +{{- end }} diff --git a/packages/system/kubevirt/templates/vm-exportproxy-ingress.yaml b/packages/system/kubevirt/templates/vm-exportproxy-ingress.yaml index a089f5ef..fc19e16f 100644 --- a/packages/system/kubevirt/templates/vm-exportproxy-ingress.yaml +++ b/packages/system/kubevirt/templates/vm-exportproxy-ingress.yaml @@ -1,8 +1,9 @@ {{- $host := index .Values._cluster "root-host" }} {{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} {{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} -{{- if and (has "vm-exportproxy" $exposeServices) }} +{{- if and (has "vm-exportproxy" $exposeServices) (ne $gatewayEnabled "true") }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/packages/system/kubevirt/templates/vm-exportproxy-tlsroute.yaml b/packages/system/kubevirt/templates/vm-exportproxy-tlsroute.yaml new file mode 100644 index 00000000..d2bcbfb7 --- /dev/null +++ b/packages/system/kubevirt/templates/vm-exportproxy-tlsroute.yaml @@ -0,0 +1,23 @@ +{{- $host := index .Values._cluster "root-host" }} +{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }} +{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} +{{- if and (has "vm-exportproxy" $exposeServices) (eq $gatewayEnabled "true") }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: vm-exportproxy +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $exposeIngress }} + sectionName: tls-vm-exportproxy + hostnames: + - {{ printf "vm-exportproxy.%s" $host | quote }} + rules: + - backendRefs: + - name: vm-exportproxy + port: 443 +{{- end }} From b719cf67e3428ccf0d7f52dcec19584df93f432d Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 19:55:59 +0300 Subject: [PATCH 13/54] feat(tenant,harbor,bucket): wire per-tenant apps through the tenant Gateway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tenants can now opt their own apps into Gateway API without touching the publishing tenant. The mechanism mirrors the existing _namespace.ingress contract exactly. packages/apps/tenant/templates/namespace.yaml: - Compute a new $gateway value: if the tenant sets .Values.gateway it becomes the tenant's own namespace, otherwise it inherits from the parent namespace (empty means no Gateway anywhere in the chain). - Ship $gateway out through the cozystack-values Secret under _namespace.gateway and through a namespace label namespace.cozystack.io/gateway. packages/apps/harbor and packages/system/bucket: - Existing Ingress wrapped in '{{ if not $gateway }}' so it only renders when no Gateway is attached anywhere up the tenant chain. - New templates/httproute.yaml render when $gateway is non-empty: apiVersion gateway.networking.k8s.io/v1 HTTPRoute, parentRef to the 'cozystack' Gateway in whichever namespace $gateway points at, hostname . (harbor) or . (bucket), backendRef to the existing Service on its current port. Per-tenant resolution: a tenant with .Values.gateway=true gets its own Gateway in its own namespace, and harbor/bucket deployed inside that tenant attach to that namespace's Gateway — no cross-namespace references, no coupling to the publishing tenant. Default remains ingress-nginx: tenants that do not opt in keep rendering Ingress verbatim. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/httproute.yaml | 21 +++++++++++++++++++ packages/apps/harbor/templates/ingress.yaml | 3 +++ packages/apps/tenant/templates/namespace.yaml | 7 +++++++ .../system/bucket/templates/httproute.yaml | 20 ++++++++++++++++++ packages/system/bucket/templates/ingress.yaml | 3 +++ 5 files changed, 54 insertions(+) create mode 100644 packages/apps/harbor/templates/httproute.yaml create mode 100644 packages/system/bucket/templates/httproute.yaml diff --git a/packages/apps/harbor/templates/httproute.yaml b/packages/apps/harbor/templates/httproute.yaml new file mode 100644 index 00000000..972f0464 --- /dev/null +++ b/packages/apps/harbor/templates/httproute.yaml @@ -0,0 +1,21 @@ +{{- $gateway := .Values._namespace.gateway | default "" }} +{{- $host := .Values._namespace.host }} +{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }} +{{- if $gateway }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .Release.Name }} +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $gateway }} + hostnames: + - {{ $harborHost | quote }} + rules: + - backendRefs: + - name: {{ .Release.Name }} + port: 80 +{{- end }} diff --git a/packages/apps/harbor/templates/ingress.yaml b/packages/apps/harbor/templates/ingress.yaml index 70933f5f..a8ac6463 100644 --- a/packages/apps/harbor/templates/ingress.yaml +++ b/packages/apps/harbor/templates/ingress.yaml @@ -1,8 +1,10 @@ {{- $ingress := .Values._namespace.ingress }} +{{- $gateway := .Values._namespace.gateway | default "" }} {{- $host := .Values._namespace.host }} {{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }} {{- $solver := (index .Values._cluster "solver") | default "http01" }} {{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} +{{- if not $gateway }} --- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -35,3 +37,4 @@ spec: name: {{ .Release.Name }} port: number: 80 +{{- end }} diff --git a/packages/apps/tenant/templates/namespace.yaml b/packages/apps/tenant/templates/namespace.yaml index dfb83730..0e884934 100644 --- a/packages/apps/tenant/templates/namespace.yaml +++ b/packages/apps/tenant/templates/namespace.yaml @@ -29,6 +29,11 @@ {{- $ingress = $tenantName }} {{- end }} +{{- $gateway := $parentNamespace.gateway | default "" }} +{{- if .Values.gateway }} +{{- $gateway = $tenantName }} +{{- end }} + {{- $monitoring := $parentNamespace.monitoring | default "" }} {{- if .Values.monitoring }} {{- $monitoring = $tenantName }} @@ -61,6 +66,7 @@ metadata: {{/* Labels for network policies */}} namespace.cozystack.io/etcd: {{ $etcd | quote }} namespace.cozystack.io/ingress: {{ $ingress | quote }} + namespace.cozystack.io/gateway: {{ $gateway | quote }} namespace.cozystack.io/monitoring: {{ $monitoring | quote }} namespace.cozystack.io/seaweedfs: {{ $seaweedfs | quote }} namespace.cozystack.io/host: {{ $computedHost | quote }} @@ -92,6 +98,7 @@ stringData: _namespace: etcd: {{ $etcd | quote }} ingress: {{ $ingress | quote }} + gateway: {{ $gateway | quote }} monitoring: {{ $monitoring | quote }} seaweedfs: {{ $seaweedfs | quote }} host: {{ $computedHost | quote }} diff --git a/packages/system/bucket/templates/httproute.yaml b/packages/system/bucket/templates/httproute.yaml new file mode 100644 index 00000000..8e6fae51 --- /dev/null +++ b/packages/system/bucket/templates/httproute.yaml @@ -0,0 +1,20 @@ +{{- $host := .Values._namespace.host }} +{{- $gateway := .Values._namespace.gateway | default "" }} +{{- if $gateway }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .Values.bucketName }}-ui +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + namespace: {{ $gateway }} + hostnames: + - {{ printf "%s.%s" .Values.bucketName $host | quote }} + rules: + - backendRefs: + - name: {{ .Values.bucketName }}-ui + port: 8080 +{{- end }} diff --git a/packages/system/bucket/templates/ingress.yaml b/packages/system/bucket/templates/ingress.yaml index b7ffaf8b..c2d80647 100644 --- a/packages/system/bucket/templates/ingress.yaml +++ b/packages/system/bucket/templates/ingress.yaml @@ -1,7 +1,9 @@ {{- $host := .Values._namespace.host }} {{- $ingress := .Values._namespace.ingress }} +{{- $gateway := .Values._namespace.gateway | default "" }} {{- $solver := (index .Values._cluster "solver") | default "http01" }} {{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} +{{- if not $gateway }} apiVersion: networking.k8s.io/v1 kind: Ingress @@ -32,3 +34,4 @@ spec: name: {{ .Values.bucketName }}-ui port: number: 8080 +{{- end }} From 1cddf71288ec8c8e12f85554758e9ccf61d2dae1 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 19:56:45 +0300 Subject: [PATCH 14/54] feat(cozystack-basics): reject tenant-* namespaces in gateway.attachedNamespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render-time guard at the top of gateway-hostname-policy.yaml walks _cluster.gateway-attached-namespaces and fails the chart render with an explicit message if any entry begins with 'tenant-'. Adding a tenant namespace to the attached whitelist would defeat the whole point of the whitelist: that tenant could then attach arbitrary HTTPRoutes to the publishing tenant's Gateway and pick any hostname inside the apex domain. Catching this at 'helm template' time beats catching it after the Gateway listener is already programmed and traffic can flow. The message spells out the correct recourse — child tenants that need their own Gateway must flip tenant.spec.gateway=true, which materialises their own Gateway in their own namespace rather than borrowing the publishing tenant's. Applies whether gateway.enabled is true or false — validation on the attachedNamespaces list is orthogonal to whether the policy itself is installed. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index f45c5067..e25c9c1d 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -1,6 +1,13 @@ {{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} {{- $apexHost := (index .Values._cluster "root-host") | default "" }} {{- $publishingNamespace := (index .Values._cluster "expose-ingress") | default "tenant-root" }} +{{- $attachedNamespaces := (index .Values._cluster "gateway-attached-namespaces") | default "" }} +{{- range splitList "," $attachedNamespaces }} + {{- $ns := . | trim }} + {{- if and $ns (hasPrefix "tenant-" $ns) }} + {{- fail (printf "publishing.gateway.attachedNamespaces must not contain tenant-* namespaces (got %q). Adding a tenant namespace here lets that tenant attach HTTPRoutes to the publishing tenant's Gateway and hijack hostnames. Child tenants that need their own Gateway must opt in via tenant.spec.gateway=true instead." $ns) }} + {{- end }} +{{- end }} {{- if and (eq $gatewayEnabled "true") $apexHost }} --- apiVersion: v1 From 72b2c80cf15b2fead6c14ade70e9555b469d43f3 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 19:57:47 +0300 Subject: [PATCH 15/54] docs(gateway): recommend per-tenant certificate count quota, refresh known limitations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README now shows the exact Tenant manifest snippet for capping the number of Certificate objects a tenant may create (count/certificates.cert-manager.io: 10), and calls out that the default is unlimited so operators on shared-apex multi-tenant clusters must set this before handing gateway=true to untrusted tenants. Also rewrites the 'Known limitations' section to reflect what this PR actually ships: TLS passthrough (cozystack-api, vm-exportproxy, cdi-uploadproxy) and per-tenant apps (harbor, bucket) are no longer limitations — they all migrate to Gateway API when gateway.enabled=true. What's left: - Child-tenant ACME HTTP-01 still relies on the publishing tenant's Gateway; a follow-up PR will add namespace-scoped Issuers. - Upstream application-level gaps (harbor ACL / bucket upstream features) — carried as upstream PRs, not a cozystack blocker. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/extra/gateway/README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index 673cbaa8..0ca0be65 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -33,9 +33,21 @@ A cluster where many tenants share the same apex domain can exhaust these quickl - Limit the number of simultaneous tenant Gateways per cluster via the platform's package quota, or cap it via `tenant.spec.resourceQuotas` with `count/certificates.cert-manager.io` to limit how many `Certificate` objects a tenant may create. - For bare-metal or air-gapped deployments consider an internal ACME server or the self-signed `ClusterIssuer` (`selfsigned-cluster-issuer`) that ships alongside the Let's Encrypt issuers. +Recommended tenant-level quota to contain a misbehaving tenant: + +```yaml +apiVersion: apps.cozystack.io/v1alpha1 +kind: Tenant +spec: + gateway: true + resourceQuotas: + count/certificates.cert-manager.io: "10" +``` + +The default for a fresh tenant is unlimited; operators running shared-apex multi-tenant clusters should set this explicitly (or stage it via the tenant-application default values) before opening `gateway: true` to non-trusted tenants. + ## Known limitations -- **TLS passthrough services** (`cozystack-api`, `vm-exportproxy`, `cdi-uploadproxy`) are not migrated to the Gateway. They keep rendering their existing Ingress regardless of `gateway.enabled`. A follow-up PR will add a Passthrough listener + `TLSRoute` per passthrough service. -- **Tenant-scoped apps** (`harbor`, `bucket`) are not yet wired to a tenant's own Gateway — they still use ingress-nginx even when `gateway.enabled=true`. Follow-up work needs to plumb `gateway` through `_namespace` in `packages/apps/tenant/templates/namespace.yaml` so the apps know which Gateway to attach to. - **Child-tenant ACME HTTP-01** currently relies on the publishing tenant's Gateway; a child tenant that turns on `gateway: true` but still has its issuer pointed at `letsencrypt-prod` (HTTP-01) must either share the parent Gateway or switch to `dns01`. A proper fix is a namespace-scoped `Issuer` per tenant — tracked as a follow-up. +- **Upstream application gaps** — some chart-level features (harbor ACL integrations, bucket upstream limitations) remain on ingress-nginx workflows in upstream docs; cozystack tracks those separately as upstream PRs. From 8442f4e44efcfca1fc4f8e74a0b863b1525736f7 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 19:58:39 +0300 Subject: [PATCH 16/54] test(e2e): flip gateway.enabled=true by default and extend gateway.bats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hack/e2e-install-cozystack.bats: add 'gateway: {enabled: true}' to the values used for the cozystack.cozystack-platform Package. The e2e pipeline therefore exercises the Gateway API path as its primary integration scenario — dashboard, keycloak render as HTTPRoutes; cozystack-api, vm-exportproxy, cdi-uploadproxy render as TLSRoutes; the legacy Ingresses for those services should not exist. hack/e2e-apps/gateway.bats: two new test cases. 1. 'exposed services render HTTPRoute/TLSRoute but not Ingress when gateway.enabled=true' — waits for the HTTPRoutes for dashboard and keycloak to reach Accepted, confirms the three TLSRoutes exist in their respective namespaces, and asserts (negative tests) that the old Ingress objects are gone. Catches regressions where a developer forgets to wrap an Ingress in the gateway-enabled conditional. 2. 'ValidatingAdmissionPolicy rejects Gateway with foreign hostname' — tries to apply a Gateway in tenant-test that claims dashboard.example.org (a domain outside the tenant's allowed suffix test.example.org), asserts kubectl fails with the expected VAP error and the expected message. Together these cover the two most important regressions this PR is shipping against: Ingress leaking through when it should not, and the hostname-hijacking defence silently disappearing. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 51 +++++++++++++++++++++++++++++++++ hack/e2e-install-cozystack.bats | 2 ++ 2 files changed, 53 insertions(+) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 211f9bf1..2c45a949 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -45,6 +45,57 @@ EOF kubectl -n tenant-test delete gateway/gateway-e2e-probe --ignore-not-found --timeout=1m } +@test "exposed services render HTTPRoute/TLSRoute but not Ingress when gateway.enabled=true" { + # With gateway.enabled=true in the install bundle, dashboard and keycloak + # must render HTTPRoute; cozystack-api, vm-exportproxy and cdi-uploadproxy + # must render TLSRoute; and none of them should have a legacy Ingress. + kubectl -n cozy-dashboard wait httproute/dashboard --for=condition=Accepted --timeout=2m + kubectl -n cozy-keycloak wait httproute/keycloak --for=condition=Accepted --timeout=2m + + kubectl -n default get tlsroute kubernetes-api + kubectl -n cozy-kubevirt get tlsroute vm-exportproxy + kubectl -n cozy-kubevirt-cdi get tlsroute cdi-uploadproxy + + # The old Ingress objects for these services must be absent. If they're + # still around, the 'gateway.enabled gate' did not exclude them on render. + ! kubectl -n cozy-dashboard get ingress dashboard-web-ingress 2>/dev/null + ! kubectl -n cozy-keycloak get ingress keycloak-ingress 2>/dev/null + ! kubectl -n default get ingress kubernetes 2>/dev/null + ! kubectl -n cozy-kubevirt get ingress vm-exportproxy 2>/dev/null + ! kubectl -n cozy-kubevirt-cdi get ingress cdi-uploadproxy 2>/dev/null +} + +@test "ValidatingAdmissionPolicy rejects Gateway with foreign hostname" { + # tenant-test namespace should only be allowed to publish its own + # domain suffix ('.test.example.org'); a listener hostname from the + # root tenant's apex must be denied by cozystack-gateway-hostname-policy. + run kubectl apply -f - <<'EOF' +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: hostname-hijack-probe + namespace: tenant-test +spec: + gatewayClassName: cilium + listeners: + - name: https + protocol: HTTPS + port: 443 + hostname: "dashboard.example.org" + tls: + mode: Terminate + certificateRefs: + - name: noop + allowedRoutes: + namespaces: + from: Same +EOF + # Expect kubectl to fail with an admission error + [ "$status" -ne 0 ] + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "must equal test.example.org" +} + @test "HTTPRoute with a matching parentRef reaches Accepted status" { # Put a Gateway and a route in the same namespace so allowedRoutes: Same accepts them. kubectl apply -f - <<'EOF' diff --git a/hack/e2e-install-cozystack.bats b/hack/e2e-install-cozystack.bats index cf66b73f..018739b8 100644 --- a/hack/e2e-install-cozystack.bats +++ b/hack/e2e-install-cozystack.bats @@ -46,6 +46,8 @@ spec: publishing: host: "example.org" apiServerEndpoint: "https://192.168.123.10:6443" + gateway: + enabled: true bundles: enabledPackages: - cozystack.external-dns-application From 49d896cacae0e2c16a973fcbfa7afde5b3b9c708 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 20:12:21 +0300 Subject: [PATCH 17/54] feat(gateway): per-tenant Issuer enables child-tenant ACME, VAP unconditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two correctness fixes for multi-tenant Gateway ACME, plus a small default-tightening. 1. Per-tenant Issuer (child-tenant ACME closes) packages/extra/gateway/templates/issuer.yaml (new): render a cert-manager.io/v1 Issuer named 'gateway' in the tenant's own namespace. It carries its own ACME account (privateKeySecretRef 'gateway-acme-account') and an http01.gatewayHTTPRoute solver whose parentRef points to the local Gateway 'cozystack' at sectionName http — no namespace on parentRef, so it attaches in-namespace only. packages/extra/gateway/templates/gateway.yaml: the wildcard Certificate now references issuerRef kind: Issuer, name: gateway (namespace-scoped) instead of the previous cluster-scoped letsencrypt-prod. The result: every tenant that enables tenant.spec.gateway=true gets a fully self-contained ACME flow — its own account, its own HTTPRoute, its own Gateway, no cross- namespace dependency on the publishing tenant. Child tenants with gateway: true now work end-to-end for ACME HTTP-01. The template maps 'publishing.certificates.issuerName' to a concrete ACME server URL: letsencrypt-prod → acme-v02.api.letsencrypt.org/directory, letsencrypt-stage → acme-staging-v02.api.letsencrypt.org/directory. Any other value fails the render with an explicit error pointing at the file to extend. Three new helm-unittest cases cover Certificate→Issuer linkage, staging-server selection, and the unknown-issuer fail path. 2. Hostname VAP installs unconditionally packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml: drop the 'eq gateway-enabled true' guard. The ValidatingAdmissionPolicy now installs whenever publishing.host is set, regardless of whether gateway.enabled is flipped. Rationale: Gateway CRDs ship unconditionally (gateway-api-crds is loaded in every bundle), so a tenant could create a Gateway manually even with gateway.enabled=false. The VAP must guard that path too. 3. Restore opt-in default in e2e hack/e2e-install-cozystack.bats: revert the gateway.enabled: true line so the default e2e platform configuration matches production defaults. Gateway API remains strictly opt-in. The 'exposed services render HTTPRoute/TLSRoute but not Ingress' test (which required platform-wide gateway.enabled=true) is removed; the VAP cross-tenant reject test stays and now works against the unconditionally-installed policy. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 20 ----- hack/e2e-install-cozystack.bats | 2 - packages/extra/gateway/templates/gateway.yaml | 4 +- packages/extra/gateway/templates/issuer.yaml | 27 +++++++ .../extra/gateway/tests/gateway_test.yaml | 73 +++++++++++++++++++ .../templates/gateway-hostname-policy.yaml | 8 +- 6 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 packages/extra/gateway/templates/issuer.yaml diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 2c45a949..79e41115 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -45,26 +45,6 @@ EOF kubectl -n tenant-test delete gateway/gateway-e2e-probe --ignore-not-found --timeout=1m } -@test "exposed services render HTTPRoute/TLSRoute but not Ingress when gateway.enabled=true" { - # With gateway.enabled=true in the install bundle, dashboard and keycloak - # must render HTTPRoute; cozystack-api, vm-exportproxy and cdi-uploadproxy - # must render TLSRoute; and none of them should have a legacy Ingress. - kubectl -n cozy-dashboard wait httproute/dashboard --for=condition=Accepted --timeout=2m - kubectl -n cozy-keycloak wait httproute/keycloak --for=condition=Accepted --timeout=2m - - kubectl -n default get tlsroute kubernetes-api - kubectl -n cozy-kubevirt get tlsroute vm-exportproxy - kubectl -n cozy-kubevirt-cdi get tlsroute cdi-uploadproxy - - # The old Ingress objects for these services must be absent. If they're - # still around, the 'gateway.enabled gate' did not exclude them on render. - ! kubectl -n cozy-dashboard get ingress dashboard-web-ingress 2>/dev/null - ! kubectl -n cozy-keycloak get ingress keycloak-ingress 2>/dev/null - ! kubectl -n default get ingress kubernetes 2>/dev/null - ! kubectl -n cozy-kubevirt get ingress vm-exportproxy 2>/dev/null - ! kubectl -n cozy-kubevirt-cdi get ingress cdi-uploadproxy 2>/dev/null -} - @test "ValidatingAdmissionPolicy rejects Gateway with foreign hostname" { # tenant-test namespace should only be allowed to publish its own # domain suffix ('.test.example.org'); a listener hostname from the diff --git a/hack/e2e-install-cozystack.bats b/hack/e2e-install-cozystack.bats index 018739b8..cf66b73f 100644 --- a/hack/e2e-install-cozystack.bats +++ b/hack/e2e-install-cozystack.bats @@ -46,8 +46,6 @@ spec: publishing: host: "example.org" apiServerEndpoint: "https://192.168.123.10:6443" - gateway: - enabled: true bundles: enabledPackages: - cozystack.external-dns-application diff --git a/packages/extra/gateway/templates/gateway.yaml b/packages/extra/gateway/templates/gateway.yaml index fb33546e..60aa4c7a 100644 --- a/packages/extra/gateway/templates/gateway.yaml +++ b/packages/extra/gateway/templates/gateway.yaml @@ -15,8 +15,8 @@ metadata: spec: secretName: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway-tls issuerRef: - name: {{ $issuerName }} - kind: ClusterIssuer + name: gateway + kind: Issuer dnsNames: - {{ $host | quote }} - {{ printf "*.%s" $host | quote }} diff --git a/packages/extra/gateway/templates/issuer.yaml b/packages/extra/gateway/templates/issuer.yaml new file mode 100644 index 00000000..b0b30c8f --- /dev/null +++ b/packages/extra/gateway/templates/issuer.yaml @@ -0,0 +1,27 @@ +{{- $issuerName := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} +{{- $acmeServer := "" }} +{{- if eq $issuerName "letsencrypt-prod" }} + {{- $acmeServer = "https://acme-v02.api.letsencrypt.org/directory" }} +{{- else if eq $issuerName "letsencrypt-stage" }} + {{- $acmeServer = "https://acme-staging-v02.api.letsencrypt.org/directory" }} +{{- else }} + {{- fail (printf "packages/extra/gateway currently supports publishing.certificates.issuerName=letsencrypt-prod or letsencrypt-stage (got %q). For other issuers, extend templates/issuer.yaml to map the name to the corresponding ACME server URL." $issuerName) }} +{{- end }} +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: gateway +spec: + acme: + server: {{ $acmeServer }} + privateKeySecretRef: + name: gateway-acme-account + solvers: + - http01: + gatewayHTTPRoute: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: cozystack + sectionName: http diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml index b74d7e6d..2f39345c 100644 --- a/packages/extra/gateway/tests/gateway_test.yaml +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -2,6 +2,7 @@ suite: tenant gateway templates: - templates/gateway.yaml - templates/cilium-lb-pool.yaml + - templates/issuer.yaml release: name: gateway @@ -187,6 +188,78 @@ tests: - cozy-dashboard - cozy-keycloak + - it: Certificate references the per-tenant Issuer (not a shared ClusterIssuer) so each tenant gets isolated ACME state + set: + _cluster: + expose-ingress: tenant-root + host: example.org + issuer-name: letsencrypt-prod + asserts: + - template: templates/gateway.yaml + documentIndex: 0 + equal: + path: kind + value: Certificate + - template: templates/gateway.yaml + documentIndex: 0 + equal: + path: spec.issuerRef.kind + value: Issuer + - template: templates/gateway.yaml + documentIndex: 0 + equal: + path: spec.issuerRef.name + value: gateway + - template: templates/issuer.yaml + hasDocuments: + count: 1 + - template: templates/issuer.yaml + equal: + path: kind + value: Issuer + - template: templates/issuer.yaml + equal: + path: metadata.name + value: gateway + - template: templates/issuer.yaml + equal: + path: spec.acme.server + value: https://acme-v02.api.letsencrypt.org/directory + - template: templates/issuer.yaml + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].name + value: cozystack + - template: templates/issuer.yaml + equal: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].sectionName + value: http + - template: templates/issuer.yaml + notExists: + path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].namespace + + - it: Issuer picks the staging ACME server when publishing.certificates.issuerName=letsencrypt-stage + set: + _cluster: + expose-ingress: tenant-root + host: example.org + issuer-name: letsencrypt-stage + asserts: + - template: templates/issuer.yaml + equal: + path: spec.acme.server + value: https://acme-staging-v02.api.letsencrypt.org/directory + + - it: unknown issuer-name fails chart render with an explicit error + set: + _cluster: + expose-ingress: tenant-root + host: example.org + issuer-name: my-custom-ca + asserts: + - template: templates/issuer.yaml + failedTemplate: + errorMessage: 'packages/extra/gateway currently supports publishing.certificates.issuerName=letsencrypt-prod or letsencrypt-stage (got "my-custom-ca"). For other issuers, extend templates/issuer.yaml to map the name to the corresponding ACME server URL.' + - it: HTTPS and HTTPS-apex listeners share the same allowedRoutes whitelist set: _cluster: diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index e25c9c1d..ef83e607 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -1,4 +1,3 @@ -{{- $gatewayEnabled := (index .Values._cluster "gateway-enabled") | default "false" }} {{- $apexHost := (index .Values._cluster "root-host") | default "" }} {{- $publishingNamespace := (index .Values._cluster "expose-ingress") | default "tenant-root" }} {{- $attachedNamespaces := (index .Values._cluster "gateway-attached-namespaces") | default "" }} @@ -8,7 +7,12 @@ {{- fail (printf "publishing.gateway.attachedNamespaces must not contain tenant-* namespaces (got %q). Adding a tenant namespace here lets that tenant attach HTTPRoutes to the publishing tenant's Gateway and hijack hostnames. Child tenants that need their own Gateway must opt in via tenant.spec.gateway=true instead." $ns) }} {{- end }} {{- end }} -{{- if and (eq $gatewayEnabled "true") $apexHost }} +{{/* The ValidatingAdmissionPolicy installs unconditionally — it must guard +Gateway creation even when gateway.enabled=false at the platform level, +because a tenant could still create a Gateway manually once the CRDs exist. +ConfigMap params pull apexHost/publishingNamespace from the cozystack-values +Secret so the VAP always has a concrete suffix to validate against. */}} +{{- if $apexHost }} --- apiVersion: v1 kind: ConfigMap From 0f44e2af57cc17875c93b729cdda044e116e6f99 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 20:13:03 +0300 Subject: [PATCH 18/54] docs(gateway): child-tenant ACME is no longer a limitation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the child-tenant ACME HTTP-01 entry from Known limitations in the gateway README. With the per-tenant Issuer added in the previous commit, each tenant that opts into gateway: true gets its own ACME account, its own HTTPRoute solver targeting its own Gateway, and no dependency on the publishing tenant's Gateway. In its place, document the actual remaining constraint: the per-tenant Issuer template currently supports two issuer names (letsencrypt-prod, letsencrypt-stage) — any other value fails the render with an actionable error. Supporting more ACME providers is a one-case extension of templates/issuer.yaml, so operators know exactly where to hook in. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/extra/gateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index 0ca0be65..063dde32 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -48,6 +48,6 @@ The default for a fresh tenant is unlimited; operators running shared-apex multi ## Known limitations -- **Child-tenant ACME HTTP-01** currently relies on the publishing tenant's Gateway; a child tenant that turns on `gateway: true` but still has its issuer pointed at `letsencrypt-prod` (HTTP-01) must either share the parent Gateway or switch to `dns01`. A proper fix is a namespace-scoped `Issuer` per tenant — tracked as a follow-up. - **Upstream application gaps** — some chart-level features (harbor ACL integrations, bucket upstream limitations) remain on ingress-nginx workflows in upstream docs; cozystack tracks those separately as upstream PRs. +- **Supported ACME issuers** — `publishing.certificates.issuerName` for Gateway-based tenants must be `letsencrypt-prod` or `letsencrypt-stage` (the chart maps those names to concrete ACME server URLs). To support another ACME provider, extend `templates/issuer.yaml` with an additional branch. From 31f630f763844fd169914006e92af81a52cb87eb Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 20:25:58 +0300 Subject: [PATCH 19/54] fix(gateway): read per-tenant apex from _namespace.host, not _cluster.host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two correctness fixes that make extra/gateway actually work in a real cluster, not just in the tests: 1. _cluster.host does not exist The Secret projected from platform's apps.yaml only exports _cluster.root-host — the cluster-wide publishing apex (e.g. example.org). There is no _cluster.host key. The gateway template was reading _cluster.host, which always evaluated to empty, which always tripped the 'publishing.host must be set' fail guard. extra/gateway therefore never rendered in a real cluster; the helm-unittest cases passed only because they explicitly put a host field under _cluster, which never happens in production. Switch to _namespace.host — the per-tenant apex that cozystack-basics (for tenant-root) and the tenant chart's namespace.yaml (for child tenants) both populate. 2. Guard against 'non-publishing namespace' blocked child tenants The previous 'if not (eq $exposeIngress .Release.Namespace)' guard forced extra/gateway to render only in the publishing tenant namespace. Child tenants with spec.gateway=true deploy extra/gateway into their own namespace, so the guard always fired and no child Gateway was ever materialised — despite earlier commits claiming child-tenant ACME works. The guard is removed; extra/gateway now renders in whichever namespace it is released into, which matches the per-tenant HelmRelease template. Side-effects: - CiliumLoadBalancerIPPool no longer carries the '== publishing namespace' condition — each tenant-scoped release produces its own pool, selecting its own cilium-gateway-cozystack Service by io.kubernetes.service.namespace. Different pools do not conflict as Kubernetes resources; IP allocation across pools with overlapping blocks is an operator concern (document). - issuer.yaml unchanged — the Issuer's parentRef already had no namespace, so the solver attaches to the local Gateway regardless of which tenant namespace we render in. - A new helm-unittest case exercises the child-tenant path (tenant-alice with host=alice.example.org): Certificate dnsNames, Gateway listener hostnames, allowedRoutes whitelist, and CiliumLoadBalancerIPPool selector all resolve to the child's apex and namespace. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../gateway/templates/cilium-lb-pool.yaml | 3 +- packages/extra/gateway/templates/gateway.yaml | 9 +-- .../extra/gateway/tests/gateway_test.yaml | 66 +++++++++++++++---- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/packages/extra/gateway/templates/cilium-lb-pool.yaml b/packages/extra/gateway/templates/cilium-lb-pool.yaml index 98f4303b..1d4bc15e 100644 --- a/packages/extra/gateway/templates/cilium-lb-pool.yaml +++ b/packages/extra/gateway/templates/cilium-lb-pool.yaml @@ -1,4 +1,3 @@ -{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} {{- $exposeExternalIPs := (index .Values._cluster "expose-external-ips") | default "" | nospace }} {{- $exposeIPsList := list }} {{- range splitList "," $exposeExternalIPs }} @@ -7,7 +6,7 @@ {{- $exposeIPsList = append $exposeIPsList $ip }} {{- end }} {{- end }} -{{- if and (eq $exposeIngress .Release.Namespace) $exposeIPsList }} +{{- if $exposeIPsList }} apiVersion: cilium.io/v2 kind: CiliumLoadBalancerIPPool metadata: diff --git a/packages/extra/gateway/templates/gateway.yaml b/packages/extra/gateway/templates/gateway.yaml index 60aa4c7a..955ae427 100644 --- a/packages/extra/gateway/templates/gateway.yaml +++ b/packages/extra/gateway/templates/gateway.yaml @@ -1,11 +1,6 @@ -{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }} -{{- $host := (index .Values._cluster "host") | default "" }} -{{- $issuerName := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} -{{- if not (eq $exposeIngress .Release.Namespace) }} -{{- fail (printf "packages/extra/gateway is only meaningful in the publishing namespace %q; current release namespace is %q. Set tenant.spec.gateway=true on that tenant only." $exposeIngress .Release.Namespace) }} -{{- end }} +{{- $host := (index .Values._namespace "host") | default "" }} {{- if not $host }} -{{- fail "publishing.host must be set in the platform chart for packages/extra/gateway to render a wildcard TLS listener." }} +{{- fail "_namespace.host is empty — the tenant has no apex domain. Either set tenant.spec.host on this tenant or inherit it from a parent that has host set." }} {{- end }} --- apiVersion: cert-manager.io/v1 diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml index 2f39345c..2fa40cad 100644 --- a/packages/extra/gateway/tests/gateway_test.yaml +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -14,8 +14,9 @@ tests: _cluster: expose-ingress: tenant-root expose-external-ips: "192.0.2.10,192.0.2.11" - host: example.org issuer-name: letsencrypt-prod + _namespace: + host: example.org asserts: - template: templates/gateway.yaml hasDocuments: @@ -85,6 +86,7 @@ tests: _cluster: expose-ingress: tenant-root expose-external-ips: "2001:db8::1,192.0.2.10" + _namespace: host: example.org asserts: - template: templates/cilium-lb-pool.yaml @@ -99,6 +101,7 @@ tests: _cluster: expose-ingress: tenant-root expose-external-ips: "192.0.2.10,," + _namespace: host: example.org asserts: - template: templates/cilium-lb-pool.yaml @@ -112,6 +115,7 @@ tests: _cluster: expose-ingress: tenant-root expose-external-ips: "" + _namespace: host: example.org asserts: - template: templates/gateway.yaml @@ -121,32 +125,63 @@ tests: hasDocuments: count: 0 - - it: release in a non-publishing namespace fails with an explicit error + - it: child tenant renders its own Gateway with the child's per-tenant apex set: _cluster: expose-ingress: tenant-root - host: example.org + expose-external-ips: "192.0.2.11" + _namespace: + host: alice.example.org release: - namespace: tenant-other + namespace: tenant-alice asserts: - template: templates/gateway.yaml - failedTemplate: - errorMessage: 'packages/extra/gateway is only meaningful in the publishing namespace "tenant-root"; current release namespace is "tenant-other". Set tenant.spec.gateway=true on that tenant only.' + hasDocuments: + count: 3 + - template: templates/gateway.yaml + documentIndex: 0 + equal: + path: spec.dnsNames + value: + - alice.example.org + - "*.alice.example.org" + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[1].hostname + value: "*.alice.example.org" + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[2].hostname + value: "alice.example.org" + - template: templates/gateway.yaml + documentIndex: 1 + equal: + path: spec.listeners[0].allowedRoutes.namespaces.selector.matchExpressions[0].values + value: + - tenant-alice + - template: templates/cilium-lb-pool.yaml + equal: + path: spec.serviceSelector.matchLabels["io.kubernetes.service.namespace"] + value: tenant-alice - - it: empty host fails with explicit error + - it: empty _namespace.host fails with explicit error set: _cluster: expose-ingress: tenant-root + _namespace: host: "" asserts: - template: templates/gateway.yaml failedTemplate: - errorMessage: "publishing.host must be set in the platform chart for packages/extra/gateway to render a wildcard TLS listener." + errorMessage: "_namespace.host is empty — the tenant has no apex domain. Either set tenant.spec.host on this tenant or inherit it from a parent that has host set." - it: allowedRoutes whitelist defaults to publishing namespace only when no extra namespaces are set set: _cluster: expose-ingress: tenant-root + _namespace: host: example.org asserts: - template: templates/gateway.yaml @@ -175,8 +210,9 @@ tests: set: _cluster: expose-ingress: tenant-root - host: example.org gateway-attached-namespaces: "cozy-cert-manager,,cozy-dashboard, cozy-keycloak," + _namespace: + host: example.org asserts: - template: templates/gateway.yaml documentIndex: 1 @@ -192,8 +228,9 @@ tests: set: _cluster: expose-ingress: tenant-root - host: example.org issuer-name: letsencrypt-prod + _namespace: + host: example.org asserts: - template: templates/gateway.yaml documentIndex: 0 @@ -241,8 +278,9 @@ tests: set: _cluster: expose-ingress: tenant-root - host: example.org issuer-name: letsencrypt-stage + _namespace: + host: example.org asserts: - template: templates/issuer.yaml equal: @@ -253,8 +291,9 @@ tests: set: _cluster: expose-ingress: tenant-root - host: example.org issuer-name: my-custom-ca + _namespace: + host: example.org asserts: - template: templates/issuer.yaml failedTemplate: @@ -264,8 +303,9 @@ tests: set: _cluster: expose-ingress: tenant-root - host: example.org gateway-attached-namespaces: "cozy-dashboard" + _namespace: + host: example.org asserts: - template: templates/gateway.yaml documentIndex: 1 From 3869a83983d0655bfbf921469d23c7dd86536ef1 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 20:27:34 +0300 Subject: [PATCH 20/54] =?UTF-8?q?feat(cozystack-basics):=20runtime=20VAP?= =?UTF-8?q?=20pair=20=E2=80=94=20per-namespace=20host=20+=20Package=20atta?= =?UTF-8?q?chedNamespaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the earlier ConfigMap-parameterised hostname VAP with two independent ValidatingAdmissionPolicies that both enforce at runtime and do not depend on the publisher's assumption that 'tenant host is always .'. 1. cozystack-gateway-hostname-policy Now reads the tenant's real apex from the namespace label namespace.cozystack.io/host, which the tenant chart already writes onto every child tenant namespace and which this commit adds to tenant-root itself. No CRD lookup, no paramRef, no assumption that the tenant host is a subdomain of the cluster-wide publishing.host. CEL: tenantHost := namespaceObject.metadata.labels['namespace.cozystack.io/host'] every listener.hostname must equal tenantHost or endsWith '.' + tenantHost Consequence: - tenant-alice with spec.host = alice.example.org gets label alice.example.org → may publish alice.example.org and *.alice.example.org, nothing else. - tenant-corp with spec.host = customer1.io (fully independent domain, NOT a subdomain of the platform apex) gets label customer1.io → may publish customer1.io and *.customer1.io. Previous VAP would have rejected this legitimate case. - A namespace without the label (no cozystack-managed tenant) falls through tenantHost == '' → all listener hostnames rejected. 2. cozystack-gateway-attached-namespaces-policy (new) Runtime counterpart to the render-time 'fail' guard on _cluster.gateway-attached-namespaces. Watches Package CRs (cozystack.io/v1alpha1) and rejects any UPDATE that introduces a tenant-* entry under spec.components.platform.values.gateway.attachedNamespaces. 'kubectl edit packages.cozystack.io cozystack.cozystack-platform' bypasses the Helm render path; this policy catches it. CEL walks the array via .all(ns, !ns.startsWith('tenant-')) with full path presence guards so it ignores packages that do not touch the gateway block. 3. tenant-root namespace label cozystack-basics/templates/tenant-root.yaml now writes labels.namespace.cozystack.io/host = {{ _cluster.root-host }} so root tenant's Gateway resources get validated through the same label-based VAP as child tenants. No ConfigMap-driven param, no split path between root and child. Both policies use failurePolicy: Fail and validationActions: [Deny]. They install unconditionally (no gateway.enabled gate) — Gateway CRDs are installed on every bundle variant, so the policies must guard every path. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 90 ++++++++++--------- .../templates/tenant-root.yaml | 2 + 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index ef83e607..826aa7e9 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -1,5 +1,3 @@ -{{- $apexHost := (index .Values._cluster "root-host") | default "" }} -{{- $publishingNamespace := (index .Values._cluster "expose-ingress") | default "tenant-root" }} {{- $attachedNamespaces := (index .Values._cluster "gateway-attached-namespaces") | default "" }} {{- range splitList "," $attachedNamespaces }} {{- $ns := . | trim }} @@ -7,23 +5,6 @@ {{- fail (printf "publishing.gateway.attachedNamespaces must not contain tenant-* namespaces (got %q). Adding a tenant namespace here lets that tenant attach HTTPRoutes to the publishing tenant's Gateway and hijack hostnames. Child tenants that need their own Gateway must opt in via tenant.spec.gateway=true instead." $ns) }} {{- end }} {{- end }} -{{/* The ValidatingAdmissionPolicy installs unconditionally — it must guard -Gateway creation even when gateway.enabled=false at the platform level, -because a tenant could still create a Gateway manually once the CRDs exist. -ConfigMap params pull apexHost/publishingNamespace from the cozystack-values -Secret so the VAP always has a concrete suffix to validate against. */}} -{{- if $apexHost }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: cozystack-gateway-hostname-policy - namespace: cozy-system - labels: - internal.cozystack.io/managed-by-cozystack: "" -data: - apexHost: {{ $apexHost | quote }} - publishingNamespace: {{ $publishingNamespace | quote }} --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy @@ -39,34 +20,26 @@ spec: apiVersions: ["v1", "v1beta1"] operations: ["CREATE", "UPDATE"] resources: ["gateways"] - paramKind: - apiVersion: v1 - kind: ConfigMap variables: - - name: apexHost - expression: "params.data.apexHost" - - name: publishingNamespace - expression: "params.data.publishingNamespace" - - name: tenantSuffix + - name: tenantHost expression: >- - object.metadata.namespace == variables.publishingNamespace - ? variables.apexHost - : (object.metadata.namespace.startsWith("tenant-") - ? object.metadata.namespace.substring(7) + "." + variables.apexHost - : "") + (has(namespaceObject.metadata.labels) && + has(namespaceObject.metadata.labels["namespace.cozystack.io/host"])) + ? namespaceObject.metadata.labels["namespace.cozystack.io/host"] + : "" validations: - expression: >- - variables.tenantSuffix != "" && + variables.tenantHost != "" && (!has(object.spec.listeners) || object.spec.listeners.all(l, !has(l.hostname) || - l.hostname == variables.tenantSuffix || - l.hostname.endsWith("." + variables.tenantSuffix) + l.hostname == variables.tenantHost || + l.hostname.endsWith("." + variables.tenantHost) )) messageExpression: >- - "Gateway listener hostname must equal " + variables.tenantSuffix + - " or end with ." + variables.tenantSuffix + - " (namespace " + object.metadata.namespace + " may only publish its own tenant's domain)" + "Gateway listener hostname must equal " + variables.tenantHost + + " or end with ." + variables.tenantHost + + " (namespace " + object.metadata.namespace + " carries label namespace.cozystack.io/host=" + variables.tenantHost + ")" reason: Forbidden --- apiVersion: admissionregistration.k8s.io/v1 @@ -77,9 +50,40 @@ metadata: internal.cozystack.io/managed-by-cozystack: "" spec: policyName: cozystack-gateway-hostname-policy - paramRef: - name: cozystack-gateway-hostname-policy - namespace: cozy-system - parameterNotFoundAction: Deny validationActions: [Deny] -{{- end }} +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: cozystack-gateway-attached-namespaces-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["cozystack.io"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["packages"] + validations: + - expression: >- + !has(object.spec.components) || + !has(object.spec.components.platform) || + !has(object.spec.components.platform.values) || + !has(object.spec.components.platform.values.gateway) || + !has(object.spec.components.platform.values.gateway.attachedNamespaces) || + object.spec.components.platform.values.gateway.attachedNamespaces.all(ns, !ns.startsWith("tenant-")) + messageExpression: >- + "spec.components.platform.values.gateway.attachedNamespaces must not contain any tenant-* namespace; use tenant.spec.gateway=true instead of adding a tenant namespace to the publishing Gateway's whitelist" + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: cozystack-gateway-attached-namespaces-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + policyName: cozystack-gateway-attached-namespaces-policy + validationActions: [Deny] diff --git a/packages/system/cozystack-basics/templates/tenant-root.yaml b/packages/system/cozystack-basics/templates/tenant-root.yaml index 93f5129e..4569139a 100644 --- a/packages/system/cozystack-basics/templates/tenant-root.yaml +++ b/packages/system/cozystack-basics/templates/tenant-root.yaml @@ -3,6 +3,8 @@ apiVersion: v1 kind: Namespace metadata: name: tenant-root + labels: + namespace.cozystack.io/host: {{ index .Values._cluster "root-host" | quote }} --- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease From f9041fa49ab4e4803b56e8f9960d14ecd5142916 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 20:33:18 +0300 Subject: [PATCH 21/54] feat(cozystack-basics): two more runtime VAPs close the tenant.spec.host and namespace label hijack vectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security review left two residual trust-assumption gaps: 1. tenant.spec.host is user-controlled. A tenant admin with RBAC permission to create Tenant CRs could claim an arbitrary host (dashboard.example.org), have the tenant chart write that host into their namespace label, and thereby satisfy the per-namespace host VAP with a hijacked value. 2. namespace.cozystack.io/host is written by cozystack charts but there is no runtime check that prevents a later update to the label from an untrusted principal. A tenant with Update on their own namespace could rewrite the label to whatever they want and bypass the per-namespace host VAP. Both close with parallel VAPs. 'Make it impossible — forbid it twice.' cozystack-tenant-host-policy matchConstraints: apps.cozystack.io/v1alpha1 tenants, CREATE/UPDATE. Rejects setting or changing spec.host unless the caller is cluster-admin (system:masters group) or a cozystack/Flux/kube-system service account. Tenants are still free to leave spec.host empty (the normal default) — the tenant chart will compute the inherited host from the parent namespace. Admins who need to override do so explicitly via kubectl / cozystack-api. cozystack-namespace-host-label-policy matchConstraints: core/v1 namespaces, UPDATE only. Rejects any change to the namespace.cozystack.io/host label once it has been set, unless the caller is in the same trusted caller whitelist. CREATE is unrestricted (initial label write happens there, by the chart). Result: the label is effectively write-once-by-cozystack — no tenant can rewrite their own host label after it is provisioned. Both policies use failurePolicy: Fail and validationActions: [Deny], so a missing paramRef or CEL error fails closed. Combined with the two earlier VAPs (Gateway listener hostname + Package attachedNamespaces) and the listener allowedRoutes namespace whitelist, the hijack surface for multi-tenant Gateway API is now guarded at four independent admission points and one runtime-proxy allow-list. Bypass requires compromising a trusted service account or obtaining cluster-admin credentials — at which point Gateway API isolation is not the weakest link anyway. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 826aa7e9..cc91f4bb 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -87,3 +87,95 @@ metadata: spec: policyName: cozystack-gateway-attached-namespaces-policy validationActions: [Deny] +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: cozystack-tenant-host-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apps.cozystack.io"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["tenants"] + variables: + - name: hostChanged + expression: >- + !has(object.spec) || !has(object.spec.host) || object.spec.host == "" || + (has(oldObject) && has(oldObject.spec) && has(oldObject.spec.host) && oldObject.spec.host == object.spec.host) ? + false : true + - name: trustedCaller + expression: >- + (has(request.userInfo.groups) && request.userInfo.groups.exists(g, g == "system:masters")) || + request.userInfo.username.startsWith("system:serviceaccount:cozy-") || + request.userInfo.username.startsWith("system:serviceaccount:flux-system:") || + request.userInfo.username.startsWith("system:serviceaccount:kube-system:") + validations: + - expression: "!variables.hostChanged || variables.trustedCaller" + messageExpression: >- + "tenant.spec.host can only be set or changed by cluster-admins (system:masters) or by cozystack/Flux service accounts; request user is " + request.userInfo.username + ". A tenant with empty spec.host inherits the apex from its parent tenant." + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: cozystack-tenant-host-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + policyName: cozystack-tenant-host-policy + validationActions: [Deny] +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: cozystack-namespace-host-label-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["namespaces"] + variables: + - name: oldHost + expression: >- + has(oldObject.metadata.labels) && has(oldObject.metadata.labels["namespace.cozystack.io/host"]) + ? oldObject.metadata.labels["namespace.cozystack.io/host"] + : "" + - name: newHost + expression: >- + has(object.metadata.labels) && has(object.metadata.labels["namespace.cozystack.io/host"]) + ? object.metadata.labels["namespace.cozystack.io/host"] + : "" + - name: trustedCaller + expression: >- + (has(request.userInfo.groups) && request.userInfo.groups.exists(g, g == "system:masters")) || + request.userInfo.username.startsWith("system:serviceaccount:cozy-") || + request.userInfo.username.startsWith("system:serviceaccount:flux-system:") || + request.userInfo.username.startsWith("system:serviceaccount:kube-system:") + validations: + - expression: >- + variables.oldHost == "" || + variables.newHost == variables.oldHost || + variables.trustedCaller + messageExpression: >- + "namespace label namespace.cozystack.io/host is immutable once set (was " + variables.oldHost + ", requested " + variables.newHost + "); only cluster-admins and cozystack/Flux service accounts may change it. Request user: " + request.userInfo.username + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: cozystack-namespace-host-label-policy + labels: + internal.cozystack.io/managed-by-cozystack: "" +spec: + policyName: cozystack-namespace-host-label-policy + validationActions: [Deny] From 74046751cbe3af8ff297b7768072ecb1a227c7fb Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 21:09:57 +0300 Subject: [PATCH 22/54] fix(cozystack-basics): gate VAP validations through matchConditions for safe CEL evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three of the four ValidatingAdmissionPolicies walked deeply into object fields inside validation CEL expressions. With failurePolicy: Fail and non-structural CRD schemas (Package, Tenant) the evaluator could reject the request instead of skipping — which would cascade into HelmReleases never being produced and the e2e install hanging on 'waiting for more than 10 HelmReleases'. Move path existence checks into matchConditions (Kubernetes 1.30+). matchConditions are evaluated before validations; a false matchCondition cleanly skips the validation rather than forcing failurePolicy. Switch to the 'key in map' operator where possible — it is the supported safe form for Kubernetes JSON without structural schema guarantees. Policies fixed: - cozystack-gateway-attached-namespaces-policy: matchCondition 'has-gateway-attached-namespaces' checks every level of the path spec.components.platform.values.gateway.attachedNamespaces using 'key in map'. Validation runs only on Packages that actually carry the field. - cozystack-tenant-host-policy: matchCondition 'has-host-field' skips Tenants without a non-empty spec.host. The hostChanged variable no longer needs defensive has() checks. - cozystack-namespace-host-label-policy: matchCondition 'had-host-label' skips namespaces that never had the namespace.cozystack.io/host label. The immutability rule now only fires on true UPDATE-of-set-label. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index cc91f4bb..02bc88bd 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -66,13 +66,17 @@ spec: apiVersions: ["v1alpha1"] operations: ["CREATE", "UPDATE"] resources: ["packages"] + matchConditions: + - name: has-gateway-attached-namespaces + expression: >- + has(object.spec) && + has(object.spec.components) && + "platform" in object.spec.components && + has(object.spec.components.platform.values) && + "gateway" in object.spec.components.platform.values && + "attachedNamespaces" in object.spec.components.platform.values.gateway validations: - expression: >- - !has(object.spec.components) || - !has(object.spec.components.platform) || - !has(object.spec.components.platform.values) || - !has(object.spec.components.platform.values.gateway) || - !has(object.spec.components.platform.values.gateway.attachedNamespaces) || object.spec.components.platform.values.gateway.attachedNamespaces.all(ns, !ns.startsWith("tenant-")) messageExpression: >- "spec.components.platform.values.gateway.attachedNamespaces must not contain any tenant-* namespace; use tenant.spec.gateway=true instead of adding a tenant namespace to the publishing Gateway's whitelist" @@ -102,12 +106,14 @@ spec: apiVersions: ["v1alpha1"] operations: ["CREATE", "UPDATE"] resources: ["tenants"] + matchConditions: + - name: has-host-field + expression: >- + has(object.spec) && "host" in object.spec && object.spec.host != "" variables: - name: hostChanged expression: >- - !has(object.spec) || !has(object.spec.host) || object.spec.host == "" || - (has(oldObject) && has(oldObject.spec) && has(oldObject.spec.host) && oldObject.spec.host == object.spec.host) ? - false : true + (has(oldObject) && has(oldObject.spec) && "host" in oldObject.spec && oldObject.spec.host == object.spec.host) ? false : true - name: trustedCaller expression: >- (has(request.userInfo.groups) && request.userInfo.groups.exists(g, g == "system:masters")) || @@ -144,15 +150,16 @@ spec: apiVersions: ["v1"] operations: ["UPDATE"] resources: ["namespaces"] + matchConditions: + - name: had-host-label + expression: >- + has(oldObject.metadata.labels) && "namespace.cozystack.io/host" in oldObject.metadata.labels variables: - name: oldHost - expression: >- - has(oldObject.metadata.labels) && has(oldObject.metadata.labels["namespace.cozystack.io/host"]) - ? oldObject.metadata.labels["namespace.cozystack.io/host"] - : "" + expression: 'oldObject.metadata.labels["namespace.cozystack.io/host"]' - name: newHost expression: >- - has(object.metadata.labels) && has(object.metadata.labels["namespace.cozystack.io/host"]) + has(object.metadata.labels) && "namespace.cozystack.io/host" in object.metadata.labels ? object.metadata.labels["namespace.cozystack.io/host"] : "" - name: trustedCaller @@ -162,10 +169,7 @@ spec: request.userInfo.username.startsWith("system:serviceaccount:flux-system:") || request.userInfo.username.startsWith("system:serviceaccount:kube-system:") validations: - - expression: >- - variables.oldHost == "" || - variables.newHost == variables.oldHost || - variables.trustedCaller + - expression: "variables.newHost == variables.oldHost || variables.trustedCaller" messageExpression: >- "namespace label namespace.cozystack.io/host is immutable once set (was " + variables.oldHost + ", requested " + variables.newHost + "); only cluster-admins and cozystack/Flux service accounts may change it. Request user: " + request.userInfo.username reason: Forbidden From 9ea31dbcfb103763b97f238fe85262c72c8dc9e1 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 22:33:32 +0300 Subject: [PATCH 23/54] fix(platform): move gateway-api-crds include out of the variant prologue to avoid YAML comment+separator collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundle file begins with a variant-switching '# Networking' comment followed by '{{- if eq .Values.bundles.system.variant ...}}'. The left-strip on that if eats the newline after '# Networking' and the next emitted '---' from the first package helper ends up appended to the comment as '# Networking---', which YAML treats as pure comment (no document separator). That hides the separator between whatever was emitted before the variant block and the first Package inside it, so the two documents merge and duplicate-key errors fire at Flux post-render time. Pre-existing behaviour was safe only because the variant block itself produced the first cozystack.io Package, so no previous Package existed to duplicate keys with. The previous commit put a brand-new 'cozystack.gateway-api-crds' Package include right before the '# Networking' comment — that Package's apiVersion then collided with the networking Package's apiVersion because the separator between them was consumed by the comment. Move the gateway-api-crds include down to the 'Common Packages' section, which has no strip-dash interaction with surrounding comments. Package ordering in the bundle does not determine install ordering — Flux honours the dependsOn chain on cozystack.networking and cozystack.cert-manager, both of which declare a dependsOn on cozystack.gateway-api-crds. Verified locally that the rendered platform chart now emits every Package preceded by '# Source:' comment on its own line, with the helper-emitted '---' on a dedicated line. 'apiVersion' keys no longer duplicate within any single YAML document. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/platform/templates/bundles/system.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/platform/templates/bundles/system.yaml b/packages/core/platform/templates/bundles/system.yaml index ec7ed834..e901b09d 100644 --- a/packages/core/platform/templates/bundles/system.yaml +++ b/packages/core/platform/templates/bundles/system.yaml @@ -1,8 +1,5 @@ {{- if .Values.bundles.system.enabled }} -# Gateway API CRDs (consumed by Cilium Gateway API controller and cert-manager) -{{include "cozystack.platform.package.default" (list "cozystack.gateway-api-crds" $) }} - # Networking {{- if eq .Values.bundles.system.variant "isp-full" }} {{- $networkingComponents := dict -}} @@ -121,6 +118,7 @@ {{- end }} # Common Packages +{{include "cozystack.platform.package.default" (list "cozystack.gateway-api-crds" $) }} {{include "cozystack.platform.package.default" (list "cozystack.cert-manager" $) }} {{include "cozystack.platform.package.default" (list "cozystack.flux-plunger" $) }} {{include "cozystack.platform.package.default" (list "cozystack.victoria-metrics-operator" $) }} From 21a3f21777341b8bbbe65905d116154e2499609b Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:42:39 +0300 Subject: [PATCH 24/54] fix(cozystack-basics): gate gateway-hostname-policy with matchConditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml:22: the first VAP had no matchConditions, so with failurePolicy: Fail and a validation expression of variables.tenantHost != "", any Gateway in a namespace without the namespace.cozystack.io/host label (kube-system, default, any non-cozystack namespace) was rejected cluster-wide, including clusters running with gateway.enabled: false. Hoist the namespace-label precondition into matchConditions so the VAP only fires for cozystack-managed tenant namespaces, then simplify the validation body — the explicit tenantHost-nonempty check becomes redundant. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 02bc88bd..f0ae6b97 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -20,22 +20,23 @@ spec: apiVersions: ["v1", "v1beta1"] operations: ["CREATE", "UPDATE"] resources: ["gateways"] + matchConditions: + - name: is-cozystack-managed-namespace + expression: >- + has(namespaceObject.metadata.labels) && + "namespace.cozystack.io/host" in namespaceObject.metadata.labels && + namespaceObject.metadata.labels["namespace.cozystack.io/host"] != "" variables: - name: tenantHost - expression: >- - (has(namespaceObject.metadata.labels) && - has(namespaceObject.metadata.labels["namespace.cozystack.io/host"])) - ? namespaceObject.metadata.labels["namespace.cozystack.io/host"] - : "" + expression: 'namespaceObject.metadata.labels["namespace.cozystack.io/host"]' validations: - expression: >- - variables.tenantHost != "" && - (!has(object.spec.listeners) || - object.spec.listeners.all(l, - !has(l.hostname) || - l.hostname == variables.tenantHost || - l.hostname.endsWith("." + variables.tenantHost) - )) + !has(object.spec.listeners) || + object.spec.listeners.all(l, + !has(l.hostname) || + l.hostname == variables.tenantHost || + l.hostname.endsWith("." + variables.tenantHost) + ) messageExpression: >- "Gateway listener hostname must equal " + variables.tenantHost + " or end with ." + variables.tenantHost + From 57f1c82aea256216651cd5eb84524c4e626ed62e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:43:37 +0300 Subject: [PATCH 25/54] fix(cozystack-basics): narrow trustedCaller to explicit cozystack group list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml:120: startsWith("system:serviceaccount:cozy-") without a trailing colon trusted any ServiceAccount in any namespace whose name begins with cozy- — including a hypothetical attacker-controlled cozy-evil namespace. Creating such a namespace requires cluster-admin today, so this is not an active escalation path, but it is an unnecessarily wide trust boundary for a defense-in-depth check. Switch both tenant-host-policy and namespace-host-label-policy to a group-based check that matches the exact system:serviceaccounts: group for cozy-system, cozy-cert-manager, flux-system and kube-system. Every SA in those namespaces is a member of that group automatically, so there is no behaviour regression for cozystack's own controllers. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index f0ae6b97..8ec900e1 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -117,10 +117,12 @@ spec: (has(oldObject) && has(oldObject.spec) && "host" in oldObject.spec && oldObject.spec.host == object.spec.host) ? false : true - name: trustedCaller expression: >- - (has(request.userInfo.groups) && request.userInfo.groups.exists(g, g == "system:masters")) || - request.userInfo.username.startsWith("system:serviceaccount:cozy-") || - request.userInfo.username.startsWith("system:serviceaccount:flux-system:") || - request.userInfo.username.startsWith("system:serviceaccount:kube-system:") + has(request.userInfo.groups) && request.userInfo.groups.exists(g, + g == "system:masters" || + g == "system:serviceaccounts:cozy-system" || + g == "system:serviceaccounts:cozy-cert-manager" || + g == "system:serviceaccounts:flux-system" || + g == "system:serviceaccounts:kube-system") validations: - expression: "!variables.hostChanged || variables.trustedCaller" messageExpression: >- @@ -165,10 +167,12 @@ spec: : "" - name: trustedCaller expression: >- - (has(request.userInfo.groups) && request.userInfo.groups.exists(g, g == "system:masters")) || - request.userInfo.username.startsWith("system:serviceaccount:cozy-") || - request.userInfo.username.startsWith("system:serviceaccount:flux-system:") || - request.userInfo.username.startsWith("system:serviceaccount:kube-system:") + has(request.userInfo.groups) && request.userInfo.groups.exists(g, + g == "system:masters" || + g == "system:serviceaccounts:cozy-system" || + g == "system:serviceaccounts:cozy-cert-manager" || + g == "system:serviceaccounts:flux-system" || + g == "system:serviceaccounts:kube-system") validations: - expression: "variables.newHost == variables.oldHost || variables.trustedCaller" messageExpression: >- From 49bc8c487addbd364a0fe5c4bd5470d76bbb2225 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:43:51 +0300 Subject: [PATCH 26/54] fix(dashboard): bind HTTPRoute to the https listener only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/system/dashboard/templates/httproute.yaml:11: without sectionName, the HTTPRoute attached to both http:80 and https:443. Under Gateway API hostname-precedence rules, a route with a specific hostname wins over the generic http-to-https-redirect HTTPRoute on the http listener, so HTTP traffic to http://dashboard.example.org/ was proxied directly to the backend with no TLS — a regression against ingress-nginx behaviour. Pin sectionName: https so port 80 stays fully owned by the redirect route. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/dashboard/templates/httproute.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/system/dashboard/templates/httproute.yaml b/packages/system/dashboard/templates/httproute.yaml index 4a6cc4c9..40c72ac0 100644 --- a/packages/system/dashboard/templates/httproute.yaml +++ b/packages/system/dashboard/templates/httproute.yaml @@ -13,6 +13,7 @@ spec: kind: Gateway name: cozystack namespace: {{ $exposeIngress }} + sectionName: https hostnames: - {{ printf "dashboard.%s" $host | quote }} rules: From 4a5e856f7b334904b4d961f3d2df758e585135fb Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:44:02 +0300 Subject: [PATCH 27/54] fix(keycloak): bind HTTPRoute to the https listener only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/system/keycloak/templates/httproute.yaml:11: same HTTP->HTTPS redirect regression as dashboard's HTTPRoute — without sectionName, the route attached to both listeners and a specific-hostname route on port 80 won the precedence contest against the generic redirect HTTPRoute. Pin sectionName: https so the keycloak route stays on the TLS-terminating listener exclusively. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/keycloak/templates/httproute.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/system/keycloak/templates/httproute.yaml b/packages/system/keycloak/templates/httproute.yaml index d6d9bb58..594260f0 100644 --- a/packages/system/keycloak/templates/httproute.yaml +++ b/packages/system/keycloak/templates/httproute.yaml @@ -13,6 +13,7 @@ spec: kind: Gateway name: cozystack namespace: {{ $exposeIngress }} + sectionName: https hostnames: - {{ $ingressHost | quote }} rules: From 966659903f54f6d7221e3871866da68b3132d5e8 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:44:13 +0300 Subject: [PATCH 28/54] fix(harbor): bind HTTPRoute to the https listener only Address review feedback from kitsunoff on packages/apps/harbor/templates/httproute.yaml:10: same HTTP->HTTPS redirect regression as dashboard's HTTPRoute. Pin sectionName: https so port 80 stays owned by the generic redirect HTTPRoute on the tenant Gateway. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/harbor/templates/httproute.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apps/harbor/templates/httproute.yaml b/packages/apps/harbor/templates/httproute.yaml index 972f0464..d7e2a632 100644 --- a/packages/apps/harbor/templates/httproute.yaml +++ b/packages/apps/harbor/templates/httproute.yaml @@ -12,6 +12,7 @@ spec: kind: Gateway name: cozystack namespace: {{ $gateway }} + sectionName: https hostnames: - {{ $harborHost | quote }} rules: From 8aeeab0ae238b62bdc68d704a27b4b4163f487f9 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:44:25 +0300 Subject: [PATCH 29/54] fix(bucket): bind HTTPRoute to the https listener only Address review feedback from kitsunoff on packages/system/bucket/templates/httproute.yaml:9: same HTTP->HTTPS redirect regression as dashboard's HTTPRoute. Pin sectionName: https so the bucket UI HTTPRoute stays on the TLS listener and the generic http-to-https-redirect on port 80 actually fires for cleartext requests. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/bucket/templates/httproute.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/system/bucket/templates/httproute.yaml b/packages/system/bucket/templates/httproute.yaml index 8e6fae51..cadc88d8 100644 --- a/packages/system/bucket/templates/httproute.yaml +++ b/packages/system/bucket/templates/httproute.yaml @@ -11,6 +11,7 @@ spec: kind: Gateway name: cozystack namespace: {{ $gateway }} + sectionName: https hostnames: - {{ printf "%s.%s" .Values.bucketName $host | quote }} rules: From e60549e7dab347ac5e66c5960784206ff6cf1f3f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:45:10 +0300 Subject: [PATCH 30/54] fix(tenant): stop inheriting parent gateway reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/apps/tenant/templates/namespace.yaml:32: the previous $gateway := $parentNamespace.gateway | default "" pattern made a child tenant without gateway: true inherit the parent's gateway namespace into _namespace.gateway. Harbor and Bucket in that child would then render HTTPRoutes with parentRefs.namespace pointing at the parent, which fails in two ways: 1. The parent Gateway's listener allowedRoutes selector whitelists only the parent namespace plus publishing.gateway.attachedNamespaces (a static cozy-* list). The child tenant namespace is not there, so the HTTPRoute ends up NotAllowedByListeners. 2. The parent wildcard Certificate covers and *. only. A child tenant apex is ., so a harbor hostname would be harbor.. — two subdomain levels deep — and no valid cert would be presented. Drop the inheritance and require explicit tenant.spec.gateway=true on each tenant that should host Gateway-attached apps. Tenants without their own gateway fall back to the inherited ingress reference, which is the same behaviour the ingress path already has. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/apps/tenant/templates/namespace.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/apps/tenant/templates/namespace.yaml b/packages/apps/tenant/templates/namespace.yaml index 0e884934..34a1d39c 100644 --- a/packages/apps/tenant/templates/namespace.yaml +++ b/packages/apps/tenant/templates/namespace.yaml @@ -29,7 +29,19 @@ {{- $ingress = $tenantName }} {{- end }} -{{- $gateway := $parentNamespace.gateway | default "" }} +{{/* + Gateway reference is NOT inherited from parent. Unlike ingress (where nginx + serves from the publishing namespace and any tenant can attach via the shared + apex), a Gateway's allowedRoutes.namespaces selector is restrictive: a child + tenant namespace is not on the parent Gateway's whitelist, so an HTTPRoute + from the child would be NotAllowedByListeners. The parent Gateway's cert also + only covers and *. (one level), and a child apex is + . — two levels deep — so even if admission let the route + through, TLS would break. Require explicit tenant.spec.gateway=true on each + tenant that wants Gateway-attached apps; otherwise apps fall back to the + inherited Ingress. +*/}} +{{- $gateway := "" }} {{- if .Values.gateway }} {{- $gateway = $tenantName }} {{- end }} From ccde31cc8b103a25a0a13ac9965b226653f2b589 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:45:45 +0300 Subject: [PATCH 31/54] fix(platform): add default namespace to gateway.attachedNamespaces Address review feedback from kitsunoff on packages/system/cozystack-api/templates/api-tlsroute.yaml:10: the kubernetes-api TLSRoute lives in the default namespace (it has to sit next to the kubernetes Service it points at), but default was missing from the platform's gateway.attachedNamespaces default list. The tenant Gateway's listener allowedRoutes selector then rejected the TLSRoute, so Kubernetes API through Gateway API silently failed to work. Add default to the default whitelist, plus a comment explaining why it has to be there. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/core/platform/values.yaml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 1a675ae7..2f1b5002 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -117,12 +117,16 @@ gateway: # materialises the Gateway itself. This flag only controls platform-wide # Gateway API integration (cert-manager solver + per-service HTTPRoutes). enabled: false - # Namespaces that are allowed to attach HTTPRoutes to a tenant Gateway. - # The publishing tenant namespace is always included implicitly. Additional - # entries must be the exact namespace name (matched on the built-in - # kubernetes.io/metadata.name label). Tenants not on this list cannot - # attach an HTTPRoute to somebody else's Gateway, which prevents hostname - # hijacking across tenants. + # Namespaces that are allowed to attach HTTPRoute or TLSRoute to a tenant + # Gateway. The publishing tenant namespace is always included implicitly. + # Additional entries must be the exact namespace name (matched on the + # built-in kubernetes.io/metadata.name label). Tenants not on this list + # cannot attach a route to somebody else's Gateway, which prevents + # hostname hijacking across tenants. + # + # The `default` namespace is included because the Kubernetes API's + # TLSRoute (shipped by the cozystack-api package) has to live next to + # the `kubernetes` Service it points at, which is always in `default`. attachedNamespaces: - cozy-cert-manager - cozy-dashboard @@ -134,6 +138,7 @@ gateway: - cozy-kubevirt-cdi - cozy-monitoring - cozy-linstor-gui + - default # Pod scheduling configuration scheduling: globalAppTopologySpreadConstraints: "" From afa69148cb00e4848da16e5200fa393db1c0fc92 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:46:26 +0300 Subject: [PATCH 32/54] docs(gateway): rewrite security model to cover all four VAPs plus the render-time check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/extra/gateway/README.md:17: the security model section described only two layers, but packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml ships four ValidatingAdmissionPolicies plus a render-time fail in cozystack-basics. Rewrite the section to cover each VAP's scope and what it guards against, plus the listener allowedRoutes whitelist and the render-time fail — six layers, each one explained. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/extra/gateway/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index 063dde32..b170e962 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -14,10 +14,16 @@ Per-tenant Gateway API Gateway backed by Cilium. Installed automatically when `t ## Security model -Two layers protect cross-tenant isolation: +Six independent layers protect cross-tenant isolation. Compromising one of them does not bypass the others; admission-time checks (layers 2–5) fail closed (`failurePolicy: Fail`, `validationActions: [Deny]`). -1. **Namespace whitelist on listeners.** The Gateway's `allowedRoutes.namespaces.from: Selector` only accepts HTTPRoutes from the publishing tenant's namespace and from `publishing.gateway.attachedNamespaces` in the platform chart (default: cozy-cert-manager, cozy-dashboard, cozy-keycloak, cozy-system, cozy-harbor, cozy-bucket, cozy-kubevirt, cozy-kubevirt-cdi, cozy-monitoring, cozy-linstor-gui). A tenant namespace that is not on this list cannot attach an HTTPRoute to this Gateway at all. -2. **Hostname ownership via ValidatingAdmissionPolicy.** `cozystack-gateway-hostname-policy` (installed by cozystack-basics when `gateway.enabled` is true) rejects any Gateway whose listener hostnames fall outside the tenant's own domain suffix. For `tenant-root` the allowed suffix is `publishing.host`; for any `tenant-` it is `.publishing.host`. Other namespaces are rejected outright. +1. **Namespace whitelist on listeners.** The Gateway's `allowedRoutes.namespaces.from: Selector` matches the built-in `kubernetes.io/metadata.name` label (written by kube-apiserver, unspoofable). It accepts routes from the publishing tenant's namespace plus `publishing.gateway.attachedNamespaces` in the platform chart (default includes the `cozy-*` namespaces for platform services and `default` for the Kubernetes API TLSRoute). A namespace outside the list literally cannot attach any `HTTPRoute` or `TLSRoute` to this Gateway. +2. **`cozystack-gateway-hostname-policy`** — `ValidatingAdmissionPolicy` on `gateway.networking.k8s.io/v1 Gateway` CREATE/UPDATE. Reads `namespaceObject.metadata.labels["namespace.cozystack.io/host"]` and rejects any listener hostname that is not equal to that value or a subdomain of it. `matchConditions` gate the VAP to cozystack-managed namespaces only — Gateways in unrelated namespaces (e.g. `kube-system`) are not touched. +3. **`cozystack-gateway-attached-namespaces-policy`** — VAP on `cozystack.io/v1alpha1 Package` CREATE/UPDATE. Rejects any `tenant-*` entry in `spec.components.platform.values.gateway.attachedNamespaces`. Catches direct `kubectl edit packages.cozystack.io` that would bypass the helm render-time guard in layer 6. +4. **`cozystack-tenant-host-policy`** — VAP on `apps.cozystack.io/v1alpha1 Tenant` CREATE/UPDATE. Rejects setting or changing `spec.host` unless the caller's groups contain `system:masters`, `system:serviceaccounts:cozy-system`, `system:serviceaccounts:cozy-cert-manager`, `system:serviceaccounts:flux-system` or `system:serviceaccounts:kube-system`. Closes the path where a tenant user sets `spec.host=dashboard.example.org` on their own tenant to have the tenant chart write a hijacked label into the namespace. +5. **`cozystack-namespace-host-label-policy`** — VAP on core `v1 Namespace` UPDATE. Rejects any change to the `namespace.cozystack.io/host` label once it is set, except by the same trusted-caller whitelist as layer 4. CREATE is unrestricted because the initial label write is part of the cozystack chart apply. +6. **Render-time `fail` in cozystack-basics.** The cozystack-basics chart fails the helm render if `_cluster.gateway-attached-namespaces` contains any `tenant-*` entry. Triggers on the helm-install path before the cluster ever sees the values — complements layer 3 which triggers at `kubectl apply` time. + +For `tenant-root` the allowed host suffix is `publishing.host`; for any `tenant-` that inherits from its parent the suffix is `.`. A child tenant with an independent apex (`customer1.io` instead of a subdomain) is handled correctly because the VAP reads the per-namespace label rather than assuming a subdomain hierarchy. ## Rate limits From 824be9354ca0190ef7d512ac3540412ba8ee3d4a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:47:13 +0300 Subject: [PATCH 33/54] test(e2e): cover the three remaining VAPs in gateway.bats Address review feedback from kitsunoff on hack/e2e-apps/gateway.bats:79: only cozystack-gateway-hostname-policy was exercised end-to-end. Commit 8770b87 specifically retrofitted matchConditions onto the other three VAPs to stop failurePolicy: Fail from turning missing fields into denials, which is a high-regression-risk area, so each VAP needs its own test. Add: - cozystack-gateway-attached-namespaces-policy rejects a Package with a tenant-* entry in spec.components.platform.values.gateway.attachedNamespaces. - cozystack-tenant-host-policy rejects a Tenant.spec.host set by an impersonated tenant-namespace ServiceAccount (non-trusted group list). - cozystack-namespace-host-label-policy rejects a kubectl label overwrite on namespace.cozystack.io/host by the same non-trusted SA. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 79e41115..f31f6056 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -76,6 +76,62 @@ EOF echo "$output" | grep -q "must equal test.example.org" } +@test "cozystack-gateway-attached-namespaces-policy rejects Packages with tenant-* entries" { + # The platform Package default name is cozystack.cozystack-platform, managed by + # cozystack-api. Creating a dummy Package with tenant-alice in gateway.attachedNamespaces + # must fail at admission time. + run kubectl apply -f - <<'EOF' +apiVersion: cozystack.io/v1alpha1 +kind: Package +metadata: + name: vap-reject-probe +spec: + variant: isp-full + components: + platform: + values: + gateway: + attachedNamespaces: + - tenant-alice +EOF + [ "$status" -ne 0 ] + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "must not contain any tenant-\*" +} + +@test "cozystack-tenant-host-policy blocks non-trusted callers from setting tenant.spec.host" { + # Impersonate a tenant-scoped ServiceAccount that is NOT in the trustedCaller + # group list. Attempt to create a Tenant with spec.host set → rejected. + run kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ + apply -f - <<'EOF' +apiVersion: apps.cozystack.io/v1alpha1 +kind: Tenant +metadata: + name: vap-host-probe + namespace: tenant-test +spec: + host: foreign.example.org +EOF + [ "$status" -ne 0 ] + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "spec.host can only be set" +} + +@test "cozystack-namespace-host-label-policy blocks non-trusted callers from changing the host label" { + # tenant-test namespace already has namespace.cozystack.io/host set by the + # cozystack tenant chart. An unprivileged SA must not be able to overwrite it. + run kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ + label namespace tenant-test \ + namespace.cozystack.io/host=foreign.example.org --overwrite + [ "$status" -ne 0 ] + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "immutable" +} + @test "HTTPRoute with a matching parentRef reaches Accepted status" { # Put a Gateway and a route in the same namespace so allowedRoutes: Same accepts them. kubectl apply -f - <<'EOF' From f5066e977a874e84190e06b613007c31c9e0f1d2 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Thu, 23 Apr 2026 23:49:24 +0300 Subject: [PATCH 34/54] fix(gateway): fail render when publishing.externalIPs is empty and guard pre-CIDR input Address review feedback from kitsunoff on packages/extra/gateway/templates/cilium-lb-pool.yaml:9: the previous behaviour rendered the tenant Gateway without a matching CiliumLoadBalancerIPPool when publishing.externalIPs was empty. Cilium LB IPAM then had no pool to serve the tenant's cilium-gateway-cozystack Service, so the Service sat in forever with no address and the operator got a silently non-working Gateway. Replace the conditional skip with an explicit helm fail that names the root cause. Also guard the CIDR suffix append on the absence of "/" to accept pre-CIDR input (consistent with the same guard already shipped on packages/extra/ingress/templates/cilium-lb-pool.yaml). Update the unit test that asserted hasDocuments: count: 0 to assert the new failed-template error message, and add expose-external-ips to every other test so they don't accidentally cross the empty-IPs guard. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../extra/gateway/templates/cilium-lb-pool.yaml | 7 ++++--- packages/extra/gateway/tests/gateway_test.yaml | 16 ++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/extra/gateway/templates/cilium-lb-pool.yaml b/packages/extra/gateway/templates/cilium-lb-pool.yaml index 1d4bc15e..43819a4a 100644 --- a/packages/extra/gateway/templates/cilium-lb-pool.yaml +++ b/packages/extra/gateway/templates/cilium-lb-pool.yaml @@ -6,7 +6,9 @@ {{- $exposeIPsList = append $exposeIPsList $ip }} {{- end }} {{- end }} -{{- if $exposeIPsList }} +{{- if not $exposeIPsList }} +{{- fail "publishing.externalIPs is empty — a tenant Gateway has no CiliumLoadBalancerIPPool to draw from. The Gateway Service would stay in with no address. Set publishing.externalIPs on the platform (or publishing.exposure=loadBalancer with a BGP / L2-announce pool you manage separately) before enabling tenant.spec.gateway=true." }} +{{- end }} apiVersion: cilium.io/v2 kind: CiliumLoadBalancerIPPool metadata: @@ -14,10 +16,9 @@ metadata: spec: blocks: {{- range $exposeIPsList }} - - cidr: {{ . }}/{{ if contains ":" . }}128{{ else }}32{{ end }} + - cidr: {{ . }}{{ if not (contains "/" .) }}/{{ if contains ":" . }}128{{ else }}32{{ end }}{{ end }} {{- end }} serviceSelector: matchLabels: "io.kubernetes.service.namespace": {{ .Release.Namespace | quote }} "gateway.networking.k8s.io/gateway-name": cozystack -{{- end }} diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml index 2fa40cad..34cbab21 100644 --- a/packages/extra/gateway/tests/gateway_test.yaml +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -110,7 +110,7 @@ tests: value: - cidr: 192.0.2.10/32 - - it: no LoadBalancer pool is rendered when externalIPs is empty but Gateway still renders + - it: chart render fails when publishing.externalIPs is empty so the operator never ships a silently non-working Gateway set: _cluster: expose-ingress: tenant-root @@ -118,12 +118,9 @@ tests: _namespace: host: example.org asserts: - - template: templates/gateway.yaml - hasDocuments: - count: 3 - template: templates/cilium-lb-pool.yaml - hasDocuments: - count: 0 + failedTemplate: + errorMessage: "publishing.externalIPs is empty — a tenant Gateway has no CiliumLoadBalancerIPPool to draw from. The Gateway Service would stay in with no address. Set publishing.externalIPs on the platform (or publishing.exposure=loadBalancer with a BGP / L2-announce pool you manage separately) before enabling tenant.spec.gateway=true." - it: child tenant renders its own Gateway with the child's per-tenant apex set: @@ -170,6 +167,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" _namespace: host: "" asserts: @@ -181,6 +179,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" _namespace: host: example.org asserts: @@ -210,6 +209,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" gateway-attached-namespaces: "cozy-cert-manager,,cozy-dashboard, cozy-keycloak," _namespace: host: example.org @@ -228,6 +228,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" issuer-name: letsencrypt-prod _namespace: host: example.org @@ -278,6 +279,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" issuer-name: letsencrypt-stage _namespace: host: example.org @@ -291,6 +293,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" issuer-name: my-custom-ca _namespace: host: example.org @@ -303,6 +306,7 @@ tests: set: _cluster: expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" gateway-attached-namespaces: "cozy-dashboard" _namespace: host: example.org From e4963d47f48ceb86850a972ff6b0edba97f9d87a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 01:50:06 +0300 Subject: [PATCH 35/54] =?UTF-8?q?fix(cozystack-basics):=20has(oldObject)?= =?UTF-8?q?=20is=20not=20valid=20CEL=20=E2=80=94=20use=20oldObject=20!=3D?= =?UTF-8?q?=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cozystack-tenant-host-policy VAP failed to install because its CEL variable used has(oldObject), which is not a valid CEL macro: has() requires a field selection expression like has(x.y), not a bare identifier check. The apiserver rejected the policy with "invalid argument to has() macro", Helm install of cozystack-basics failed, Flux triggered uninstall remediation which deleted the namespaces created by the chart (cozy-public, tenant-root), and every downstream HelmRelease that had to create resources in cozy-public or tenant-root (kubevirt-cdi, the tenant chart, etc.) then failed with "namespace ... because it is being terminated". Root cause for the whole install-cozystack E2E regression on this branch. Replace has(oldObject) with oldObject != null. On CREATE oldObject is null and the ternary returns true (hostChanged); on UPDATE oldObject is set and the rest of the condition runs as before. Found via the cozyreport artifact from run 24858285228 rerun — the kubectl describe hr cozystack-basics output exposed the VAP compilation error that the top-level HelmRelease status message truncated with "1 error occurred: ...". Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../cozystack-basics/templates/gateway-hostname-policy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 8ec900e1..0abca906 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -114,7 +114,7 @@ spec: variables: - name: hostChanged expression: >- - (has(oldObject) && has(oldObject.spec) && "host" in oldObject.spec && oldObject.spec.host == object.spec.host) ? false : true + (oldObject != null && has(oldObject.spec) && "host" in oldObject.spec && oldObject.spec.host == object.spec.host) ? false : true - name: trustedCaller expression: >- has(request.userInfo.groups) && request.userInfo.groups.exists(g, From cec5f8d1d28e6f559486f58b6985dd0b4f5bbd6f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 04:11:18 +0300 Subject: [PATCH 36/54] fix(cozystack-basics): chain has() through each level of namespaceObject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second CEL bug in the same VAP surfaced in E2E run 24862782568 (gateway bats test on tenant-test namespace): matchCondition has(namespaceObject.metadata.labels) errored at admission time with "no such key: metadata" — apiserver rejected the Gateway with "ValidatingAdmissionPolicy denied request: expression '...' resulted in error: no such key: metadata". In Kubernetes CEL the has() macro checks field presence at the last selection only — has(a.b.c) requires a.b to already be resolvable. When the intermediate step is missing or the object is represented as an unstructured map with a missing key, CEL raises 'no such key' at evaluation rather than letting has() short-circuit to false. Chain the checks per Kubernetes CEL docs recommendation (Check if a field is set → has(a.b) && has(a.b.c) && ...): namespaceObject != null && has(namespaceObject.metadata) && has(namespaceObject.metadata.labels) && "namespace.cozystack.io/host" in namespaceObject.metadata.labels && namespaceObject.metadata.labels["namespace.cozystack.io/host"] != "" The null-check handles the cluster-scoped case that K8s documents as 'namespaceObject is null when the incoming object is cluster-scoped', the has() chain handles the unstructured-map case that surfaced in the gateway-e2e-probe on tenant-test. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../cozystack-basics/templates/gateway-hostname-policy.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 0abca906..8533d7b3 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -23,6 +23,8 @@ spec: matchConditions: - name: is-cozystack-managed-namespace expression: >- + namespaceObject != null && + has(namespaceObject.metadata) && has(namespaceObject.metadata.labels) && "namespace.cozystack.io/host" in namespaceObject.metadata.labels && namespaceObject.metadata.labels["namespace.cozystack.io/host"] != "" From 74c406e3313dab5ae56e397dda379584456abc7c Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 06:15:56 +0300 Subject: [PATCH 37/54] test(e2e): rewrite VAP tests without bats run helper (cozytest.sh is pure shell) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hack/cozytest.sh is a pure-shell bats-compat runner, not real bats. The 'run' builtin that bats ships (captures cmd output/status into $output and $status) is unavailable, so every test using 'run kubectl …' fails with 'run: not found' as soon as it's exercised. Previous runs didn't surface this because install-cozystack was failing before the gateway bats file could execute — now that the install path is green, the VAP tests actually run and hit this trap. Rewrite the four VAP negative-path tests to capture kubectl output and exit status manually. Uses '&& echo __SUCCEEDED__' inside the command substitution so the happy-path (admission accepted something it shouldn't) is detectable, and captures stderr via 2>&1 so admission rejection messages land in $output for grep assertions. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index f31f6056..3ad9934c 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -49,7 +49,9 @@ EOF # tenant-test namespace should only be allowed to publish its own # domain suffix ('.test.example.org'); a listener hostname from the # root tenant's apex must be denied by cozystack-gateway-hostname-policy. - run kubectl apply -f - <<'EOF' + # hack/cozytest.sh is a pure-shell bats-compat runner — bats' `run` helper + # is NOT available, so we capture kubectl output and exit status manually. + output=$(kubectl apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -70,8 +72,9 @@ spec: namespaces: from: Same EOF - # Expect kubectl to fail with an admission error - [ "$status" -ne 0 ] +) + # Expect kubectl to fail with an admission error (no __SUCCEEDED__ marker). + echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted cross-tenant hostname" >&2; return 1; } echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "must equal test.example.org" } @@ -80,7 +83,7 @@ EOF # The platform Package default name is cozystack.cozystack-platform, managed by # cozystack-api. Creating a dummy Package with tenant-alice in gateway.attachedNamespaces # must fail at admission time. - run kubectl apply -f - <<'EOF' + output=$(kubectl apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" apiVersion: cozystack.io/v1alpha1 kind: Package metadata: @@ -94,18 +97,19 @@ spec: attachedNamespaces: - tenant-alice EOF - [ "$status" -ne 0 ] +) + echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted tenant-* in attachedNamespaces" >&2; return 1; } echo "$output" | grep -qi "ValidatingAdmissionPolicy" - echo "$output" | grep -q "must not contain any tenant-\*" + echo "$output" | grep -q "must not contain any tenant-" } @test "cozystack-tenant-host-policy blocks non-trusted callers from setting tenant.spec.host" { # Impersonate a tenant-scoped ServiceAccount that is NOT in the trustedCaller # group list. Attempt to create a Tenant with spec.host set → rejected. - run kubectl --as=system:serviceaccount:tenant-test:default \ - --as-group=system:serviceaccounts \ - --as-group=system:serviceaccounts:tenant-test \ - apply -f - <<'EOF' + output=$(kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ + apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" apiVersion: apps.cozystack.io/v1alpha1 kind: Tenant metadata: @@ -114,7 +118,8 @@ metadata: spec: host: foreign.example.org EOF - [ "$status" -ne 0 ] +) + echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted tenant.spec.host from untrusted SA" >&2; return 1; } echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "spec.host can only be set" } @@ -122,12 +127,12 @@ EOF @test "cozystack-namespace-host-label-policy blocks non-trusted callers from changing the host label" { # tenant-test namespace already has namespace.cozystack.io/host set by the # cozystack tenant chart. An unprivileged SA must not be able to overwrite it. - run kubectl --as=system:serviceaccount:tenant-test:default \ - --as-group=system:serviceaccounts \ - --as-group=system:serviceaccounts:tenant-test \ + output=$(kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ label namespace tenant-test \ - namespace.cozystack.io/host=foreign.example.org --overwrite - [ "$status" -ne 0 ] + namespace.cozystack.io/host=foreign.example.org --overwrite 2>&1 && echo "__SUCCEEDED__") + echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted host label change from untrusted SA" >&2; return 1; } echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "immutable" } From 992c67a433465a924d5e9407fb0df998f6865f8f Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 08:03:39 +0300 Subject: [PATCH 38/54] =?UTF-8?q?test(e2e):=20diagnostic=20test=20?= =?UTF-8?q?=E2=80=94=20verify=20tenant-test=20has=20host=20label=20before?= =?UTF-8?q?=20VAP=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VAP 'ValidatingAdmissionPolicy rejects Gateway with foreign hostname' silently fails to reject when tenant-test namespace lacks the namespace.cozystack.io/host label — matchCondition returns false, VAP skips, Gateway is admitted. That's what happened on run 24870368740. Add a dedicated test BEFORE the VAP tests that asserts tenant-test actually has the expected host label. If it doesn't, the test fails with a clear SETUP FAILURE message and dumps the full namespace yaml to CI logs, so the real root-cause (label missing — tenant chart bug, helm timing, or namespaceObject representation issue) is visible instead of being obscured by the downstream VAP test failure. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 3ad9934c..a23e8494 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -45,6 +45,24 @@ EOF kubectl -n tenant-test delete gateway/gateway-e2e-probe --ignore-not-found --timeout=1m } +@test "tenant-test namespace carries namespace.cozystack.io/host label from tenant chart" { + # Diagnostic: the whole hostname-policy VAP keys off this label. If it is + # missing or empty, the VAP's matchCondition returns false, VAP skips, and + # EVERY Gateway in the namespace is admitted regardless of listener hostname. + # Make that bug loud instead of letting it fall through as a silent pass. + host_label=$(kubectl get namespace tenant-test -o jsonpath='{.metadata.labels.namespace\.cozystack\.io/host}') + if [ -z "$host_label" ]; then + echo "SETUP FAILURE: tenant-test namespace lacks namespace.cozystack.io/host label" >&2 + kubectl get namespace tenant-test -o yaml >&2 + return 1 + fi + if [ "$host_label" != "test.example.org" ]; then + echo "SETUP FAILURE: tenant-test host label is '$host_label', expected 'test.example.org'" >&2 + kubectl get namespace tenant-test -o yaml >&2 + return 1 + fi +} + @test "ValidatingAdmissionPolicy rejects Gateway with foreign hostname" { # tenant-test namespace should only be allowed to publish its own # domain suffix ('.test.example.org'); a listener hostname from the From 4ef14c4147743d0f27cebdaed163eb7ccf750711 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 09:23:22 +0300 Subject: [PATCH 39/54] fix(cozystack-basics,e2e): diagnose and simplify hostname-policy matchCondition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 24870368740 showed the VAP still not rejecting cross-tenant hostname after the CEL chain fix. Diagnostic test on run 24873181209 confirmed: 1. tenant-test namespace DOES have namespace.cozystack.io/host=test.example.org. 2. admission still CREATES a Gateway with listener hostname dashboard.example.org (__SUCCEEDED__ marker appeared — kubectl apply returned 0). So the label is present but the VAP's matchCondition evaluates to false for some reason, skipping the policy entirely. The prime suspect is has(namespaceObject.metadata) on a Kubernetes unstructured representation where the has() macro semantics don't match the typed Namespace resource my chain fix assumed. Simplify the matchCondition to use only the in-operator for map-key presence ("k" in map) — this is the canonical CEL pattern for K8s-style unstructured labels and does not rely on has() working consistently across nested selection: namespaceObject != null && "namespace.cozystack.io/host" in namespaceObject.metadata.labels && namespaceObject.metadata.labels["..."] != "" Also add a diagnostic test to hack/e2e-apps/gateway.bats that dumps the VAP + binding yaml and asserts validationActions includes Deny — so if anything structural breaks with the VAP installation, the failure is visible in the first few lines of output. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 14 ++++++++++++++ .../templates/gateway-hostname-policy.yaml | 2 -- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index a23e8494..92ef5a32 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -45,6 +45,20 @@ EOF kubectl -n tenant-test delete gateway/gateway-e2e-probe --ignore-not-found --timeout=1m } +@test "cozystack-gateway-hostname-policy VAP and binding are installed" { + # Diagnose the hostname-policy enforcement path from the ground up. + # If these resources are missing, admission cannot reject anything. + kubectl get validatingadmissionpolicy cozystack-gateway-hostname-policy -o yaml + kubectl get validatingadmissionpolicybinding cozystack-gateway-hostname-policy -o yaml + # Binding MUST have validationActions: [Deny] — [Audit] / [] would let requests through silently. + actions=$(kubectl get validatingadmissionpolicybinding cozystack-gateway-hostname-policy -o jsonpath='{.spec.validationActions}') + echo "binding.validationActions=$actions" + case "$actions" in + *Deny*) ;; + *) echo "SETUP FAILURE: binding.validationActions lacks Deny (got '$actions')" >&2; return 1 ;; + esac +} + @test "tenant-test namespace carries namespace.cozystack.io/host label from tenant chart" { # Diagnostic: the whole hostname-policy VAP keys off this label. If it is # missing or empty, the VAP's matchCondition returns false, VAP skips, and diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 8533d7b3..8fc8cb5c 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -24,8 +24,6 @@ spec: - name: is-cozystack-managed-namespace expression: >- namespaceObject != null && - has(namespaceObject.metadata) && - has(namespaceObject.metadata.labels) && "namespace.cozystack.io/host" in namespaceObject.metadata.labels && namespaceObject.metadata.labels["namespace.cozystack.io/host"] != "" variables: From 3ff3d681321731f45afb7961b726316bd21a37c3 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 10:43:05 +0300 Subject: [PATCH 40/54] fix(cozystack-basics): matchCondition by namespace prefix, move label lookup to variables with ternary fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous matchConditions on namespaceObject.metadata.labels were evaluating to false or skipping silently despite the label being present (verified via diagnostic test on run 24875535058: VAP exists, binding has validationActions: [Deny], tenant-test namespace has namespace.cozystack.io/host=test.example.org, yet admission still accepted a Gateway with listener hostname dashboard.example.org). Restructure the VAP so matchCondition does NOT touch namespaceObject — it just checks the Gateway's declared namespace starts with tenant- via object.metadata.namespace. The namespaceObject-based label lookup moves into a CEL variable with a ternary that returns "" when the label is missing, and the validation accepts if tenantHost is empty (skip) or if every listener hostname matches. This way the gate fires predictably for every tenant-* Gateway and the namespaceObject access happens in a context where its shape is easier to reason about. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 8fc8cb5c..2f5c2f1a 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -21,16 +21,19 @@ spec: operations: ["CREATE", "UPDATE"] resources: ["gateways"] matchConditions: - - name: is-cozystack-managed-namespace - expression: >- - namespaceObject != null && - "namespace.cozystack.io/host" in namespaceObject.metadata.labels && - namespaceObject.metadata.labels["namespace.cozystack.io/host"] != "" + - name: tenant-namespace + expression: object.metadata.namespace.startsWith("tenant-") variables: - name: tenantHost - expression: 'namespaceObject.metadata.labels["namespace.cozystack.io/host"]' + expression: >- + (namespaceObject != null && + has(namespaceObject.metadata.labels) && + "namespace.cozystack.io/host" in namespaceObject.metadata.labels) + ? namespaceObject.metadata.labels["namespace.cozystack.io/host"] + : "" validations: - expression: >- + variables.tenantHost == "" || !has(object.spec.listeners) || object.spec.listeners.all(l, !has(l.hostname) || From 9c96dc8e7893b9278e6e8aca2508970100d0e7f1 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 11:43:53 +0300 Subject: [PATCH 41/54] =?UTF-8?q?fix(gateway):=20require=20dns01=20solver?= =?UTF-8?q?=20=E2=80=94=20Let's=20Encrypt=20refuses=20HTTP-01=20for=20wild?= =?UTF-8?q?card=20certs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from kitsunoff on packages/extra/gateway/templates/issuer.yaml:21 — the per-tenant Issuer was emitting an http01.gatewayHTTPRoute solver while the accompanying Certificate in gateway.yaml requests both the apex and the *. wildcard. Let's Encrypt refuses to issue wildcard certificates through HTTP-01 (see LE challenge-types docs and the cert-manager CRD schema note at crd-cert-manager.io_clusterissuers.yaml:728-729). Under the default publishing.certificates.solver=http01, the Order would loop forever, the Secret would never be created, and the Gateway HTTPS listeners would sit without a cert. cert-manager-issuers ClusterIssuers already branch on _cluster.solver (http01 vs dns01 Cloudflare); the per-tenant Issuer was ignoring that signal entirely and the E2E flag that would have exercised the full issuance flow was reverted in a4bef14, so CI missed the regression. Fail the render when publishing.certificates.solver=http01 with an explicit message pointing at the root cause and the dns01/selfsigned alternatives, and switch the emitted solver to dns01.cloudflare (same shape cluster-issuers.yaml uses). Add a unit test for the http01-fail path and update the existing Issuer test asserts to match the dns01 solver shape. Follow-up: add an E2E assertion that actually issues a Certificate through the per-tenant Issuer and waits for .status.conditions[Ready]=True once the gateway.enabled=true install path is re-enabled. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/extra/gateway/templates/issuer.yaml | 29 +++++++++++---- .../extra/gateway/tests/gateway_test.yaml | 35 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/extra/gateway/templates/issuer.yaml b/packages/extra/gateway/templates/issuer.yaml index b0b30c8f..44ce7bda 100644 --- a/packages/extra/gateway/templates/issuer.yaml +++ b/packages/extra/gateway/templates/issuer.yaml @@ -1,4 +1,5 @@ {{- $issuerName := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }} +{{- $solver := (index .Values._cluster "solver") | default "http01" }} {{- $acmeServer := "" }} {{- if eq $issuerName "letsencrypt-prod" }} {{- $acmeServer = "https://acme-v02.api.letsencrypt.org/directory" }} @@ -7,6 +8,22 @@ {{- else }} {{- fail (printf "packages/extra/gateway currently supports publishing.certificates.issuerName=letsencrypt-prod or letsencrypt-stage (got %q). For other issuers, extend templates/issuer.yaml to map the name to the corresponding ACME server URL." $issuerName) }} {{- end }} +{{- /* + Let's Encrypt refuses to issue wildcard certificates via HTTP-01 (see + cert-manager/cert-manager-crds/.../crd-cert-manager.io_clusterissuers.yaml + at the http01 field description, and LE policy + https://letsencrypt.org/docs/challenge-types/#http-01-challenge). The + tenant Gateway's Certificate requests apex + *.apex, so HTTP-01 would + loop the Order forever and the HTTPS listeners would never get a cert. + Require dns01 (or selfsigned) for Gateway API tenants — fail fast so + the operator sees the mismatch at install time instead of during a + stuck cert-manager challenge. +*/}} +{{- if eq $solver "http01" }} + {{- fail "publishing.certificates.solver=http01 cannot issue the wildcard certificate the tenant Gateway needs (Let's Encrypt refuses HTTP-01 for wildcard identifiers). Set publishing.certificates.solver=dns01 (with a Cloudflare API token Secret in cozy-cert-manager) before enabling tenant.spec.gateway=true, or use the self-signed ClusterIssuer for air-gapped clusters." }} +{{- else if ne $solver "dns01" }} + {{- fail (printf "packages/extra/gateway currently supports publishing.certificates.solver=dns01 only (got %q). Extend templates/issuer.yaml to add additional solver types." $solver) }} +{{- end }} --- apiVersion: cert-manager.io/v1 kind: Issuer @@ -18,10 +35,8 @@ spec: privateKeySecretRef: name: gateway-acme-account solvers: - - http01: - gatewayHTTPRoute: - parentRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: cozystack - sectionName: http + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token-secret + key: api-token diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml index 34cbab21..2490bf12 100644 --- a/packages/extra/gateway/tests/gateway_test.yaml +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -12,6 +12,7 @@ tests: - it: renders Certificate, Gateway, redirect HTTPRoute and CiliumLoadBalancerIPPool when IPs and host are set set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10,192.0.2.11" issuer-name: letsencrypt-prod @@ -84,6 +85,7 @@ tests: - it: IPv6 external IPs use /128 CIDRs set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "2001:db8::1,192.0.2.10" _namespace: @@ -99,6 +101,7 @@ tests: - it: empty entries in expose-external-ips are filtered out set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10,," _namespace: @@ -113,6 +116,7 @@ tests: - it: chart render fails when publishing.externalIPs is empty so the operator never ships a silently non-working Gateway set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "" _namespace: @@ -125,6 +129,7 @@ tests: - it: child tenant renders its own Gateway with the child's per-tenant apex set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.11" _namespace: @@ -166,6 +171,7 @@ tests: - it: empty _namespace.host fails with explicit error set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" _namespace: @@ -178,6 +184,7 @@ tests: - it: allowedRoutes whitelist defaults to publishing namespace only when no extra namespaces are set set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" _namespace: @@ -208,6 +215,7 @@ tests: - it: allowedRoutes whitelist prepends publishing namespace to the gateway-attached-namespaces list and filters empty entries set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" gateway-attached-namespaces: "cozy-cert-manager,,cozy-dashboard, cozy-keycloak," @@ -227,6 +235,7 @@ tests: - it: Certificate references the per-tenant Issuer (not a shared ClusterIssuer) so each tenant gets isolated ACME state set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" issuer-name: letsencrypt-prod @@ -265,19 +274,31 @@ tests: value: https://acme-v02.api.letsencrypt.org/directory - template: templates/issuer.yaml equal: - path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].name - value: cozystack + path: spec.acme.solvers[0].dns01.cloudflare.apiTokenSecretRef.name + value: cloudflare-api-token-secret - template: templates/issuer.yaml equal: - path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].sectionName - value: http + path: spec.acme.solvers[0].dns01.cloudflare.apiTokenSecretRef.key + value: api-token + + - it: chart render fails when publishing.certificates.solver=http01 because Let's Encrypt refuses HTTP-01 for wildcard certs + set: + _cluster: + solver: http01 + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" + issuer-name: letsencrypt-prod + _namespace: + host: example.org + asserts: - template: templates/issuer.yaml - notExists: - path: spec.acme.solvers[0].http01.gatewayHTTPRoute.parentRefs[0].namespace + failedTemplate: + errorMessage: "publishing.certificates.solver=http01 cannot issue the wildcard certificate the tenant Gateway needs (Let's Encrypt refuses HTTP-01 for wildcard identifiers). Set publishing.certificates.solver=dns01 (with a Cloudflare API token Secret in cozy-cert-manager) before enabling tenant.spec.gateway=true, or use the self-signed ClusterIssuer for air-gapped clusters." - it: Issuer picks the staging ACME server when publishing.certificates.issuerName=letsencrypt-stage set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" issuer-name: letsencrypt-stage @@ -292,6 +313,7 @@ tests: - it: unknown issuer-name fails chart render with an explicit error set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" issuer-name: my-custom-ca @@ -305,6 +327,7 @@ tests: - it: HTTPS and HTTPS-apex listeners share the same allowedRoutes whitelist set: _cluster: + solver: dns01 expose-ingress: tenant-root expose-external-ips: "192.0.2.10" gateway-attached-namespaces: "cozy-dashboard" From a622d346c69ff195cf151999249da3da82808e46 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 11:44:08 +0300 Subject: [PATCH 42/54] fix(cozystack-basics): close tenant-host-policy bypass via clearing spec.host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from coderabbitai on packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml:133 — the matchCondition required object.spec.host != "", so an UPDATE that unset or emptied the host field skipped the VAP entirely. A tenant with empty spec.host inherits the apex from its parent, so clearing a previously-set host is itself an effective apex change that the policy's intent already covers. A non-trusted caller with UPDATE permission on the Tenant could flip spec.host: foo.example.org → "" without ever hitting the trustedCaller gate. Rewrite the gate so the VAP fires whenever EITHER side of the update has a non-empty host, and compute hostChanged over both oldHost and newHost (each with its own CEL variable and ternary fallback to "" when the field is absent). Leaves the validation unchanged: the change is allowed only when hostChanged=false or variables.trustedCaller=true. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../templates/gateway-hostname-policy.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 2f5c2f1a..3ae430d5 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -113,11 +113,17 @@ spec: matchConditions: - name: has-host-field expression: >- - has(object.spec) && "host" in object.spec && object.spec.host != "" + (has(object.spec) && "host" in object.spec && object.spec.host != "") || + (oldObject != null && has(oldObject.spec) && "host" in oldObject.spec && oldObject.spec.host != "") variables: - - name: hostChanged + - name: oldHost expression: >- - (oldObject != null && has(oldObject.spec) && "host" in oldObject.spec && oldObject.spec.host == object.spec.host) ? false : true + (oldObject != null && has(oldObject.spec) && "host" in oldObject.spec) ? oldObject.spec.host : "" + - name: newHost + expression: >- + (has(object.spec) && "host" in object.spec) ? object.spec.host : "" + - name: hostChanged + expression: variables.oldHost != variables.newHost - name: trustedCaller expression: >- has(request.userInfo.groups) && request.userInfo.groups.exists(g, From 2925795a5f76a8c255c5518608965eadd2335675 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 11:44:22 +0300 Subject: [PATCH 43/54] fix(cilium): set defensive envoy DaemonSet resource baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address observation from kitsunoff on packages/system/cilium/values.yaml:21 — envoy.enabled: true turns on a per-node envoy DaemonSet but the upstream default leaves envoy.resources at {} (no requests, no limits). PR description cites ~100 MB RAM/node at idle; under L7 traffic that grows without a cap, and the lack of requests also lets the DaemonSet schedule onto any node with zero reserved headroom. Pin the baseline matching the commented-out example in the upstream Helm values: 100m CPU + 512Mi memory requests, 1Gi memory limit. That gives the scheduler something to plan around and prevents a runaway listener from OOM-ing the node without drawing attention. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/system/cilium/values.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/system/cilium/values.yaml b/packages/system/cilium/values.yaml index 81d0e843..af03c972 100644 --- a/packages/system/cilium/values.yaml +++ b/packages/system/cilium/values.yaml @@ -19,6 +19,17 @@ cilium: digest: "sha256:700f06f4803a838a8e830be5ace4650e3ad82bdefabfb2f4d110368d307a5efb" envoy: enabled: true + # Upstream default is {} — no requests/limits. The embedded envoy DaemonSet + # idles at ~100 MB RAM/node and grows under L7 traffic; without requests + # the DaemonSet schedules onto any node with no reserved headroom, and + # without a memory limit a runaway listener could OOM the node. + # The baseline matches the commented-out example in the upstream Helm values. + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + memory: 1Gi gatewayAPI: enabled: true rollOutCiliumPods: true From e036a644e71d7c1c173e1cf9de87c111d1b5a270 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 11:44:35 +0300 Subject: [PATCH 44/54] test(e2e): grant RBAC to impersonated SA in gateway VAP negative tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from coderabbitai on hack/e2e-apps/gateway.bats:170 — the VAP negative-path tests impersonate system:serviceaccount:tenant-test:default, but that SA has no RBAC to create apps.cozystack.io/tenants or to update/patch namespaces. Authorization runs BEFORE admission, so without those grants the apiserver returns a plain RBAC Forbidden, the test's grep for "ValidatingAdmissionPolicy" fails, and the test appears to be a VAP regression when the VAP is actually fine. Create an ad-hoc Role + RoleBinding in tenant-test granting create/update on apps.cozystack.io/tenants to default SA (scoped to tenant-test) for the tenant-host-policy test, and a ClusterRole + ClusterRoleBinding granting update/patch on core namespaces for the namespace-host-label policy test. Both are deleted after the test runs so the sandbox stays clean for subsequent tests. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 62 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 92ef5a32..e03562b3 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -137,7 +137,35 @@ EOF @test "cozystack-tenant-host-policy blocks non-trusted callers from setting tenant.spec.host" { # Impersonate a tenant-scoped ServiceAccount that is NOT in the trustedCaller - # group list. Attempt to create a Tenant with spec.host set → rejected. + # group list. First grant RBAC to create Tenants — authorization runs BEFORE + # admission, so without this grant the apiserver returns a plain RBAC + # Forbidden and the test would fail grep-ing for 'ValidatingAdmissionPolicy' + # even though the VAP itself is fine. + kubectl apply -f - <<'EOF' +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: vap-probe-tenant-create + namespace: tenant-test +rules: +- apiGroups: ["apps.cozystack.io"] + resources: ["tenants"] + verbs: ["create","get","list","watch","update","patch","delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: vap-probe-tenant-create + namespace: tenant-test +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: vap-probe-tenant-create +subjects: +- kind: ServiceAccount + name: default + namespace: tenant-test +EOF output=$(kubectl --as=system:serviceaccount:tenant-test:default \ --as-group=system:serviceaccounts \ --as-group=system:serviceaccounts:tenant-test \ @@ -151,6 +179,8 @@ spec: host: foreign.example.org EOF ) + kubectl -n tenant-test delete rolebinding vap-probe-tenant-create --ignore-not-found + kubectl -n tenant-test delete role vap-probe-tenant-create --ignore-not-found echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted tenant.spec.host from untrusted SA" >&2; return 1; } echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "spec.host can only be set" @@ -158,12 +188,40 @@ EOF @test "cozystack-namespace-host-label-policy blocks non-trusted callers from changing the host label" { # tenant-test namespace already has namespace.cozystack.io/host set by the - # cozystack tenant chart. An unprivileged SA must not be able to overwrite it. + # cozystack tenant chart. Grant patch on namespaces cluster-wide to the + # impersonated SA — namespaces is a cluster-scoped resource so this needs a + # ClusterRole. Authorization runs before admission, so without this grant + # the test would fail with plain RBAC Forbidden rather than a VAP rejection. + kubectl apply -f - <<'EOF' +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: vap-probe-namespace-patch +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","list","watch","update","patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: vap-probe-namespace-patch +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: vap-probe-namespace-patch +subjects: +- kind: ServiceAccount + name: default + namespace: tenant-test +EOF output=$(kubectl --as=system:serviceaccount:tenant-test:default \ --as-group=system:serviceaccounts \ --as-group=system:serviceaccounts:tenant-test \ label namespace tenant-test \ namespace.cozystack.io/host=foreign.example.org --overwrite 2>&1 && echo "__SUCCEEDED__") + kubectl delete clusterrolebinding vap-probe-namespace-patch --ignore-not-found + kubectl delete clusterrole vap-probe-namespace-patch --ignore-not-found echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted host label change from untrusted SA" >&2; return 1; } echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "immutable" From ff52afc02a14e4c7841d90ce0810ba4b5f6bb9e3 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 13:07:38 +0300 Subject: [PATCH 45/54] =?UTF-8?q?test(e2e):=20fix=20shell=20logic=20?= =?UTF-8?q?=E2=80=94=20set=20-e=20killed=20VAP=20tests=20on=20happy=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Observed on run 24880685728: the hostname-policy VAP correctly rejected the foreign-hostname Gateway (apiserver returned the exact "Gateway listener hostname must equal test.example.org" message), but the bats test still failed. Root cause: the guard echo "$output" | grep -q "__SUCCEEDED__" && { return 1; } is an AND list at statement level. When the marker is ABSENT (the intended happy path — admission rejected, no marker captured), grep exits 1, && short-circuits, the overall compound command returns 1, and cozytest.sh's set -e kills the test. Rewrite each of the four VAP negative-path guards as an if-statement: if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: …" >&2 return 1 fi Bash's set -e explicitly ignores non-zero exits from commands inside if-conditions, so the happy path falls through to the follow-up greps that verify the expected rejection message. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index e03562b3..1af77ffc 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -106,7 +106,7 @@ spec: EOF ) # Expect kubectl to fail with an admission error (no __SUCCEEDED__ marker). - echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted cross-tenant hostname" >&2; return 1; } + if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted cross-tenant hostname" >&2; return 1; fi echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "must equal test.example.org" } @@ -130,7 +130,7 @@ spec: - tenant-alice EOF ) - echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted tenant-* in attachedNamespaces" >&2; return 1; } + if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted tenant-* in attachedNamespaces" >&2; return 1; fi echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "must not contain any tenant-" } @@ -181,7 +181,7 @@ EOF ) kubectl -n tenant-test delete rolebinding vap-probe-tenant-create --ignore-not-found kubectl -n tenant-test delete role vap-probe-tenant-create --ignore-not-found - echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted tenant.spec.host from untrusted SA" >&2; return 1; } + if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted tenant.spec.host from untrusted SA" >&2; return 1; fi echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "spec.host can only be set" } @@ -222,7 +222,7 @@ EOF namespace.cozystack.io/host=foreign.example.org --overwrite 2>&1 && echo "__SUCCEEDED__") kubectl delete clusterrolebinding vap-probe-namespace-patch --ignore-not-found kubectl delete clusterrole vap-probe-namespace-patch --ignore-not-found - echo "$output" | grep -q "__SUCCEEDED__" && { echo "BUG: admission accepted host label change from untrusted SA" >&2; return 1; } + if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted host label change from untrusted SA" >&2; return 1; fi echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "immutable" } From 3a698e76d399326644a279a784151df69c647a2a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 13:39:04 +0300 Subject: [PATCH 46/54] fix(cozystack-basics): close namespace-host-label-policy CREATE gap The policy previously only fired on UPDATE and only when oldObject already carried namespace.cozystack.io/host, which left two paths unguarded: - CREATE of a namespace with the label pre-set. - UPDATE that adds the label for the first time (oldObject has no label, so the matchCondition was false and the VAP was skipped). Both paths required namespace create/update on labels, which is normally cluster-admin-only, but the VAP is meant to be the source of truth for this label's integrity. Close the gap: - operations: [CREATE, UPDATE] - matchCondition now fires when either object or oldObject carries the label (renamed had-host-label -> touches-host-label). - oldHost is computed with a null-safe ternary so CREATE (where oldObject is null) evaluates cleanly. The existing validation (newHost == oldHost || trustedCaller) then naturally denies first-time label writes from non-trusted callers while still allowing cozy-system / cozy-cert-manager / flux-system / kube-system SAs to stamp the label during the tenant chart apply. Update packages/extra/gateway/README.md layer 5 description to match and add an e2e test that asserts a namespace CREATE with the label from an untrusted SA is rejected. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 49 +++++++++++++++++++ packages/extra/gateway/README.md | 2 +- .../templates/gateway-hostname-policy.yaml | 12 +++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 1af77ffc..6bda785c 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -227,6 +227,55 @@ EOF echo "$output" | grep -q "immutable" } +@test "cozystack-namespace-host-label-policy blocks non-trusted callers from setting the host label at CREATE" { + # Defense-in-depth: a non-trusted caller must not be able to stamp + # namespace.cozystack.io/host=X on a brand-new namespace either — only + # cozystack/Flux SAs may write the label. Authorization runs before + # admission, so grant cluster-wide namespace create to the impersonated SA + # first, otherwise the test would fail with plain RBAC Forbidden. + kubectl apply -f - <<'EOF' +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: vap-probe-namespace-create +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["create","get","list","delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: vap-probe-namespace-create +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: vap-probe-namespace-create +subjects: +- kind: ServiceAccount + name: default + namespace: tenant-test +EOF + output=$(kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ + apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" +apiVersion: v1 +kind: Namespace +metadata: + name: vap-host-label-probe + labels: + namespace.cozystack.io/host: foreign.example.org +EOF +) + kubectl delete namespace vap-host-label-probe --ignore-not-found --wait=false + kubectl delete clusterrolebinding vap-probe-namespace-create --ignore-not-found + kubectl delete clusterrole vap-probe-namespace-create --ignore-not-found + if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted first-time host label write from untrusted SA at CREATE" >&2; return 1; fi + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "immutable" +} + @test "HTTPRoute with a matching parentRef reaches Accepted status" { # Put a Gateway and a route in the same namespace so allowedRoutes: Same accepts them. kubectl apply -f - <<'EOF' diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index b170e962..f8f7b026 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -20,7 +20,7 @@ Six independent layers protect cross-tenant isolation. Compromising one of them 2. **`cozystack-gateway-hostname-policy`** — `ValidatingAdmissionPolicy` on `gateway.networking.k8s.io/v1 Gateway` CREATE/UPDATE. Reads `namespaceObject.metadata.labels["namespace.cozystack.io/host"]` and rejects any listener hostname that is not equal to that value or a subdomain of it. `matchConditions` gate the VAP to cozystack-managed namespaces only — Gateways in unrelated namespaces (e.g. `kube-system`) are not touched. 3. **`cozystack-gateway-attached-namespaces-policy`** — VAP on `cozystack.io/v1alpha1 Package` CREATE/UPDATE. Rejects any `tenant-*` entry in `spec.components.platform.values.gateway.attachedNamespaces`. Catches direct `kubectl edit packages.cozystack.io` that would bypass the helm render-time guard in layer 6. 4. **`cozystack-tenant-host-policy`** — VAP on `apps.cozystack.io/v1alpha1 Tenant` CREATE/UPDATE. Rejects setting or changing `spec.host` unless the caller's groups contain `system:masters`, `system:serviceaccounts:cozy-system`, `system:serviceaccounts:cozy-cert-manager`, `system:serviceaccounts:flux-system` or `system:serviceaccounts:kube-system`. Closes the path where a tenant user sets `spec.host=dashboard.example.org` on their own tenant to have the tenant chart write a hijacked label into the namespace. -5. **`cozystack-namespace-host-label-policy`** — VAP on core `v1 Namespace` UPDATE. Rejects any change to the `namespace.cozystack.io/host` label once it is set, except by the same trusted-caller whitelist as layer 4. CREATE is unrestricted because the initial label write is part of the cozystack chart apply. +5. **`cozystack-namespace-host-label-policy`** — VAP on core `v1 Namespace` CREATE/UPDATE. Rejects any set or change of the `namespace.cozystack.io/host` label, except by the same trusted-caller whitelist as layer 4. This closes both first-time label writes on CREATE and first-time adds on UPDATE — only cozystack/Flux service accounts (which apply the tenant chart) can stamp the label. 6. **Render-time `fail` in cozystack-basics.** The cozystack-basics chart fails the helm render if `_cluster.gateway-attached-namespaces` contains any `tenant-*` entry. Triggers on the helm-install path before the cluster ever sees the values — complements layer 3 which triggers at `kubectl apply` time. For `tenant-root` the allowed host suffix is `publishing.host`; for any `tenant-` that inherits from its parent the suffix is `.`. A child tenant with an independent apex (`customer1.io` instead of a subdomain) is handled correctly because the VAP reads the per-namespace label rather than assuming a subdomain hierarchy. diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 3ae430d5..468d970a 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -160,15 +160,19 @@ spec: resourceRules: - apiGroups: [""] apiVersions: ["v1"] - operations: ["UPDATE"] + operations: ["CREATE", "UPDATE"] resources: ["namespaces"] matchConditions: - - name: had-host-label + - name: touches-host-label expression: >- - has(oldObject.metadata.labels) && "namespace.cozystack.io/host" in oldObject.metadata.labels + (has(object.metadata.labels) && "namespace.cozystack.io/host" in object.metadata.labels) || + (oldObject != null && has(oldObject.metadata.labels) && "namespace.cozystack.io/host" in oldObject.metadata.labels) variables: - name: oldHost - expression: 'oldObject.metadata.labels["namespace.cozystack.io/host"]' + expression: >- + (oldObject != null && has(oldObject.metadata.labels) && "namespace.cozystack.io/host" in oldObject.metadata.labels) + ? oldObject.metadata.labels["namespace.cozystack.io/host"] + : "" - name: newHost expression: >- has(object.metadata.labels) && "namespace.cozystack.io/host" in object.metadata.labels From ac70320544680c5af195c95fd8de2bdf6e6c7357 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 15:24:19 +0300 Subject: [PATCH 47/54] fix(cozystack-basics): Flux lives in cozy-fluxcd, not flux-system Root cause of the install regression on run 24885268333: the Flux ServiceAccount in cozystack is `system:serviceaccount:cozy-fluxcd:flux`, not `flux-system:flux`. My trustedCaller list had `system:serviceaccounts:flux-system`, so once the namespace-host-label VAP was broadened to CREATE, Flux's first-time write of `namespace.cozystack.io/host` on tenant-test got rejected: namespaces "tenant-test" is forbidden: ValidatingAdmissionPolicy 'cozystack-namespace-host-label-policy' denied request: namespace label namespace.cozystack.io/host is immutable once set (was , requested test.example.org); only cluster-admins and cozystack/Flux service accounts may change it. Request user: system:serviceaccount:cozy-fluxcd:flux Replace `flux-system` with `cozy-fluxcd` in the trustedCaller group list of both cozystack-tenant-host-policy and cozystack-namespace-host-label-policy, plus the README. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- packages/extra/gateway/README.md | 2 +- .../cozystack-basics/templates/gateway-hostname-policy.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/extra/gateway/README.md b/packages/extra/gateway/README.md index f8f7b026..e25d181e 100644 --- a/packages/extra/gateway/README.md +++ b/packages/extra/gateway/README.md @@ -19,7 +19,7 @@ Six independent layers protect cross-tenant isolation. Compromising one of them 1. **Namespace whitelist on listeners.** The Gateway's `allowedRoutes.namespaces.from: Selector` matches the built-in `kubernetes.io/metadata.name` label (written by kube-apiserver, unspoofable). It accepts routes from the publishing tenant's namespace plus `publishing.gateway.attachedNamespaces` in the platform chart (default includes the `cozy-*` namespaces for platform services and `default` for the Kubernetes API TLSRoute). A namespace outside the list literally cannot attach any `HTTPRoute` or `TLSRoute` to this Gateway. 2. **`cozystack-gateway-hostname-policy`** — `ValidatingAdmissionPolicy` on `gateway.networking.k8s.io/v1 Gateway` CREATE/UPDATE. Reads `namespaceObject.metadata.labels["namespace.cozystack.io/host"]` and rejects any listener hostname that is not equal to that value or a subdomain of it. `matchConditions` gate the VAP to cozystack-managed namespaces only — Gateways in unrelated namespaces (e.g. `kube-system`) are not touched. 3. **`cozystack-gateway-attached-namespaces-policy`** — VAP on `cozystack.io/v1alpha1 Package` CREATE/UPDATE. Rejects any `tenant-*` entry in `spec.components.platform.values.gateway.attachedNamespaces`. Catches direct `kubectl edit packages.cozystack.io` that would bypass the helm render-time guard in layer 6. -4. **`cozystack-tenant-host-policy`** — VAP on `apps.cozystack.io/v1alpha1 Tenant` CREATE/UPDATE. Rejects setting or changing `spec.host` unless the caller's groups contain `system:masters`, `system:serviceaccounts:cozy-system`, `system:serviceaccounts:cozy-cert-manager`, `system:serviceaccounts:flux-system` or `system:serviceaccounts:kube-system`. Closes the path where a tenant user sets `spec.host=dashboard.example.org` on their own tenant to have the tenant chart write a hijacked label into the namespace. +4. **`cozystack-tenant-host-policy`** — VAP on `apps.cozystack.io/v1alpha1 Tenant` CREATE/UPDATE. Rejects setting or changing `spec.host` unless the caller's groups contain `system:masters`, `system:serviceaccounts:cozy-system`, `system:serviceaccounts:cozy-cert-manager`, `system:serviceaccounts:cozy-fluxcd` or `system:serviceaccounts:kube-system`. Closes the path where a tenant user sets `spec.host=dashboard.example.org` on their own tenant to have the tenant chart write a hijacked label into the namespace. 5. **`cozystack-namespace-host-label-policy`** — VAP on core `v1 Namespace` CREATE/UPDATE. Rejects any set or change of the `namespace.cozystack.io/host` label, except by the same trusted-caller whitelist as layer 4. This closes both first-time label writes on CREATE and first-time adds on UPDATE — only cozystack/Flux service accounts (which apply the tenant chart) can stamp the label. 6. **Render-time `fail` in cozystack-basics.** The cozystack-basics chart fails the helm render if `_cluster.gateway-attached-namespaces` contains any `tenant-*` entry. Triggers on the helm-install path before the cluster ever sees the values — complements layer 3 which triggers at `kubectl apply` time. diff --git a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml index 468d970a..99be904c 100644 --- a/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml +++ b/packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml @@ -130,7 +130,7 @@ spec: g == "system:masters" || g == "system:serviceaccounts:cozy-system" || g == "system:serviceaccounts:cozy-cert-manager" || - g == "system:serviceaccounts:flux-system" || + g == "system:serviceaccounts:cozy-fluxcd" || g == "system:serviceaccounts:kube-system") validations: - expression: "!variables.hostChanged || variables.trustedCaller" @@ -184,7 +184,7 @@ spec: g == "system:masters" || g == "system:serviceaccounts:cozy-system" || g == "system:serviceaccounts:cozy-cert-manager" || - g == "system:serviceaccounts:flux-system" || + g == "system:serviceaccounts:cozy-fluxcd" || g == "system:serviceaccounts:kube-system") validations: - expression: "variables.newHost == variables.oldHost || variables.trustedCaller" From 604b84f78f46558d19cf7ad483d9d4571870536b Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 17:00:23 +0300 Subject: [PATCH 48/54] test(e2e): invert VAP rejection tests to use `if !` so set -e is respected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous pattern relied on a "__SUCCEEDED__" marker captured via `output=$(kubectl apply … && echo "__SUCCEEDED__")`. On dash (hack/cozytest.sh is #!/bin/sh with set -e) a failing command substitution propagates its non-zero exit through the assignment and kills the test before the follow-up grep can run — so admission correctly rejecting the probe read as a test failure. The marker approach also hides what actually happened behind a string comparison. Rewrite each VAP negative-path test as `if ! output=$(…); then … else …; fi`. The if-condition disables set -e per POSIX, the exit code is consumed directly (admission reject = happy path), and on the unexpected-success branch we print the captured admission response and clean up the resource that slipped through. Happy-path cleanup only removes the scaffolding RBAC because the probe resource was never created. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 120 ++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 6bda785c..38d8803b 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -81,9 +81,12 @@ EOF # tenant-test namespace should only be allowed to publish its own # domain suffix ('.test.example.org'); a listener hostname from the # root tenant's apex must be denied by cozystack-gateway-hostname-policy. - # hack/cozytest.sh is a pure-shell bats-compat runner — bats' `run` helper - # is NOT available, so we capture kubectl output and exit status manually. - output=$(kubectl apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" + # hack/cozytest.sh is /bin/sh (dash) with set -e — a failing command + # substitution propagates its exit status through variable assignment and + # kills the test. We specifically EXPECT admission to reject the apply, so + # use `if !` — set -e is disabled inside the if-condition, the exit status + # is captured, and $output is filled either way for the follow-up greps. + if ! output=$(kubectl apply -f - 2>&1 <<'EOF' apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -104,18 +107,23 @@ spec: namespaces: from: Same EOF -) - # Expect kubectl to fail with an admission error (no __SUCCEEDED__ marker). - if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted cross-tenant hostname" >&2; return 1; fi - echo "$output" | grep -qi "ValidatingAdmissionPolicy" - echo "$output" | grep -q "must equal test.example.org" +); then + # Happy path: admission rejected the Gateway. Verify the rejection came + # from our VAP and names the expected tenant host. + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "must equal test.example.org" + else + echo "BUG: admission accepted cross-tenant hostname — Gateway 'hostname-hijack-probe' was created in tenant-test" >&2 + echo "$output" >&2 + return 1 + fi } @test "cozystack-gateway-attached-namespaces-policy rejects Packages with tenant-* entries" { # The platform Package default name is cozystack.cozystack-platform, managed by # cozystack-api. Creating a dummy Package with tenant-alice in gateway.attachedNamespaces # must fail at admission time. - output=$(kubectl apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" + if ! output=$(kubectl apply -f - 2>&1 <<'EOF' apiVersion: cozystack.io/v1alpha1 kind: Package metadata: @@ -129,10 +137,15 @@ spec: attachedNamespaces: - tenant-alice EOF -) - if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted tenant-* in attachedNamespaces" >&2; return 1; fi - echo "$output" | grep -qi "ValidatingAdmissionPolicy" - echo "$output" | grep -q "must not contain any tenant-" +); then + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "must not contain any tenant-" + else + echo "BUG: admission accepted tenant-* in attachedNamespaces — Package 'vap-reject-probe' was created" >&2 + echo "$output" >&2 + kubectl delete package vap-reject-probe --ignore-not-found + return 1 + fi } @test "cozystack-tenant-host-policy blocks non-trusted callers from setting tenant.spec.host" { @@ -166,10 +179,10 @@ subjects: name: default namespace: tenant-test EOF - output=$(kubectl --as=system:serviceaccount:tenant-test:default \ - --as-group=system:serviceaccounts \ - --as-group=system:serviceaccounts:tenant-test \ - apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" + if ! output=$(kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ + apply -f - 2>&1 <<'EOF' apiVersion: apps.cozystack.io/v1alpha1 kind: Tenant metadata: @@ -178,12 +191,19 @@ metadata: spec: host: foreign.example.org EOF -) - kubectl -n tenant-test delete rolebinding vap-probe-tenant-create --ignore-not-found - kubectl -n tenant-test delete role vap-probe-tenant-create --ignore-not-found - if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted tenant.spec.host from untrusted SA" >&2; return 1; fi - echo "$output" | grep -qi "ValidatingAdmissionPolicy" - echo "$output" | grep -q "spec.host can only be set" +); then + kubectl -n tenant-test delete rolebinding vap-probe-tenant-create --ignore-not-found + kubectl -n tenant-test delete role vap-probe-tenant-create --ignore-not-found + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "spec.host can only be set" + else + kubectl -n tenant-test delete tenants.apps.cozystack.io vap-host-probe --ignore-not-found + kubectl -n tenant-test delete rolebinding vap-probe-tenant-create --ignore-not-found + kubectl -n tenant-test delete role vap-probe-tenant-create --ignore-not-found + echo "BUG: admission accepted tenant.spec.host from untrusted SA — Tenant 'vap-host-probe' was created" >&2 + echo "$output" >&2 + return 1 + fi } @test "cozystack-namespace-host-label-policy blocks non-trusted callers from changing the host label" { @@ -215,16 +235,24 @@ subjects: name: default namespace: tenant-test EOF - output=$(kubectl --as=system:serviceaccount:tenant-test:default \ - --as-group=system:serviceaccounts \ - --as-group=system:serviceaccounts:tenant-test \ + if ! output=$(kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ label namespace tenant-test \ - namespace.cozystack.io/host=foreign.example.org --overwrite 2>&1 && echo "__SUCCEEDED__") - kubectl delete clusterrolebinding vap-probe-namespace-patch --ignore-not-found - kubectl delete clusterrole vap-probe-namespace-patch --ignore-not-found - if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted host label change from untrusted SA" >&2; return 1; fi - echo "$output" | grep -qi "ValidatingAdmissionPolicy" - echo "$output" | grep -q "immutable" + namespace.cozystack.io/host=foreign.example.org --overwrite 2>&1); then + kubectl delete clusterrolebinding vap-probe-namespace-patch --ignore-not-found + kubectl delete clusterrole vap-probe-namespace-patch --ignore-not-found + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "immutable" + else + # Revert label if apiserver somehow accepted the overwrite. + kubectl label namespace tenant-test namespace.cozystack.io/host=test.example.org --overwrite + kubectl delete clusterrolebinding vap-probe-namespace-patch --ignore-not-found + kubectl delete clusterrole vap-probe-namespace-patch --ignore-not-found + echo "BUG: admission accepted host label change from untrusted SA" >&2 + echo "$output" >&2 + return 1 + fi } @test "cozystack-namespace-host-label-policy blocks non-trusted callers from setting the host label at CREATE" { @@ -256,10 +284,10 @@ subjects: name: default namespace: tenant-test EOF - output=$(kubectl --as=system:serviceaccount:tenant-test:default \ - --as-group=system:serviceaccounts \ - --as-group=system:serviceaccounts:tenant-test \ - apply -f - 2>&1 <<'EOF' && echo "__SUCCEEDED__" + if ! output=$(kubectl --as=system:serviceaccount:tenant-test:default \ + --as-group=system:serviceaccounts \ + --as-group=system:serviceaccounts:tenant-test \ + apply -f - 2>&1 <<'EOF' apiVersion: v1 kind: Namespace metadata: @@ -267,13 +295,19 @@ metadata: labels: namespace.cozystack.io/host: foreign.example.org EOF -) - kubectl delete namespace vap-host-label-probe --ignore-not-found --wait=false - kubectl delete clusterrolebinding vap-probe-namespace-create --ignore-not-found - kubectl delete clusterrole vap-probe-namespace-create --ignore-not-found - if echo "$output" | grep -q "__SUCCEEDED__"; then echo "BUG: admission accepted first-time host label write from untrusted SA at CREATE" >&2; return 1; fi - echo "$output" | grep -qi "ValidatingAdmissionPolicy" - echo "$output" | grep -q "immutable" +); then + kubectl delete clusterrolebinding vap-probe-namespace-create --ignore-not-found + kubectl delete clusterrole vap-probe-namespace-create --ignore-not-found + echo "$output" | grep -qi "ValidatingAdmissionPolicy" + echo "$output" | grep -q "immutable" + else + kubectl delete namespace vap-host-label-probe --ignore-not-found --wait=false + kubectl delete clusterrolebinding vap-probe-namespace-create --ignore-not-found + kubectl delete clusterrole vap-probe-namespace-create --ignore-not-found + echo "BUG: admission accepted first-time host label write from untrusted SA at CREATE — Namespace 'vap-host-label-probe' was created" >&2 + echo "$output" >&2 + return 1 + fi } @test "HTTPRoute with a matching parentRef reaches Accepted status" { From 206a31f89cd0d8929dd41f1f001d7fa2f741604a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 24 Apr 2026 17:07:08 +0300 Subject: [PATCH 49/54] refactor(gateway): delegate CiliumLoadBalancerIPPool to tenant chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2468 introduced packages/apps/tenant/templates/cilium-lb-pool.yaml as the per-tenant owner of the LoadBalancer IP pool. Extend that template with the .Values.gateway branch: the pool now renders when either _cluster.expose-mode=loadBalancer (with .Values.ingress=true on the publishing tenant) OR .Values.gateway=true. Cilium LB IPAM forbids overlapping CIDRs across pools regardless of serviceSelector, so a single pool per tenant is required — the same CIDRs cannot back both ingress and gateway from two separate pools without one of them being marked Conflicting. Delete packages/extra/gateway/templates/cilium-lb-pool.yaml — the gateway chart no longer renders its own pool. The empty-externalIPs render-time fail moves to the tenant template, guarded on the gateway branch so the legacy ingress path remains unchanged. Gateway tests lose the pool-rendering asserts that previously lived in packages/extra/gateway/tests/gateway_test.yaml. The tenant chart tests in packages/apps/tenant/tests/exposure_test.yaml gain five gateway-path cases covering pool rendering, non-publishing-tenant rendering, the simultaneous ingress+gateway single-pool case, gateway-only with ingress=false, and the empty-externalIPs fail. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../apps/tenant/templates/cilium-lb-pool.yaml | 6 +- packages/apps/tenant/tests/exposure_test.yaml | 75 +++++++++++++++++++ .../gateway/templates/cilium-lb-pool.yaml | 24 ------ .../extra/gateway/tests/gateway_test.yaml | 68 +---------------- 4 files changed, 81 insertions(+), 92 deletions(-) delete mode 100644 packages/extra/gateway/templates/cilium-lb-pool.yaml diff --git a/packages/apps/tenant/templates/cilium-lb-pool.yaml b/packages/apps/tenant/templates/cilium-lb-pool.yaml index 63dbcaca..28fc7a7a 100644 --- a/packages/apps/tenant/templates/cilium-lb-pool.yaml +++ b/packages/apps/tenant/templates/cilium-lb-pool.yaml @@ -12,7 +12,11 @@ (eq $exposeMode "loadBalancer") (eq $exposeIngress .Release.Namespace) .Values.ingress }} -{{- if and $isPublishingIngressLB $ipsList }} +{{- $hasOwnGateway := .Values.gateway | default false }} +{{- if and $hasOwnGateway (not $ipsList) }} +{{- fail "publishing.externalIPs is empty — a tenant Gateway has no CiliumLoadBalancerIPPool to draw from. The Gateway Service would stay in with no address. Set publishing.externalIPs on the platform (or publishing.exposure=loadBalancer with a BGP / L2-announce pool you manage separately) before enabling tenant.spec.gateway=true." }} +{{- end }} +{{- if and (or $isPublishingIngressLB $hasOwnGateway) $ipsList }} apiVersion: cilium.io/v2 kind: CiliumLoadBalancerIPPool metadata: diff --git a/packages/apps/tenant/tests/exposure_test.yaml b/packages/apps/tenant/tests/exposure_test.yaml index e86fab5b..89e0e0f5 100644 --- a/packages/apps/tenant/tests/exposure_test.yaml +++ b/packages/apps/tenant/tests/exposure_test.yaml @@ -139,3 +139,78 @@ tests: asserts: - hasDocuments: count: 0 + + - it: gateway=true in any tenant renders pool with namespace-only selector + set: + gateway: true + _cluster: + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: root-exposure + - equal: + path: spec.blocks + value: + - cidr: 192.0.2.10/32 + - equal: + path: spec.serviceSelector.matchLabels["io.kubernetes.service.namespace"] + value: tenant-root + + - it: gateway=true in a non-publishing tenant renders its own pool (not tied to publishing tenant) + set: + gateway: true + _cluster: + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" + release: + name: tenant-u1 + namespace: tenant-u1 + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: u1-exposure + - equal: + path: spec.serviceSelector.matchLabels["io.kubernetes.service.namespace"] + value: tenant-u1 + + - it: gateway=true + ingress=true + loadBalancer mode renders a single shared pool (no overlapping CIDR conflict) + set: + gateway: true + ingress: true + _cluster: + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" + expose-mode: loadBalancer + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: root-exposure + + - it: gateway=true with ingress=false still renders pool (gateway alone is enough) + set: + gateway: true + ingress: false + _cluster: + expose-ingress: tenant-root + expose-external-ips: "192.0.2.10" + asserts: + - hasDocuments: + count: 1 + + - it: gateway=true with empty externalIPs fails chart render with explicit message + set: + gateway: true + _cluster: + expose-ingress: tenant-root + expose-external-ips: "" + asserts: + - failedTemplate: + errorMessage: "publishing.externalIPs is empty — a tenant Gateway has no CiliumLoadBalancerIPPool to draw from. The Gateway Service would stay in with no address. Set publishing.externalIPs on the platform (or publishing.exposure=loadBalancer with a BGP / L2-announce pool you manage separately) before enabling tenant.spec.gateway=true." diff --git a/packages/extra/gateway/templates/cilium-lb-pool.yaml b/packages/extra/gateway/templates/cilium-lb-pool.yaml deleted file mode 100644 index 43819a4a..00000000 --- a/packages/extra/gateway/templates/cilium-lb-pool.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- $exposeExternalIPs := (index .Values._cluster "expose-external-ips") | default "" | nospace }} -{{- $exposeIPsList := list }} -{{- range splitList "," $exposeExternalIPs }} - {{- $ip := . | trim }} - {{- if $ip }} - {{- $exposeIPsList = append $exposeIPsList $ip }} - {{- end }} -{{- end }} -{{- if not $exposeIPsList }} -{{- fail "publishing.externalIPs is empty — a tenant Gateway has no CiliumLoadBalancerIPPool to draw from. The Gateway Service would stay in with no address. Set publishing.externalIPs on the platform (or publishing.exposure=loadBalancer with a BGP / L2-announce pool you manage separately) before enabling tenant.spec.gateway=true." }} -{{- end }} -apiVersion: cilium.io/v2 -kind: CiliumLoadBalancerIPPool -metadata: - name: {{ trimPrefix "tenant-" .Release.Namespace }}-gateway -spec: - blocks: - {{- range $exposeIPsList }} - - cidr: {{ . }}{{ if not (contains "/" .) }}/{{ if contains ":" . }}128{{ else }}32{{ end }}{{ end }} - {{- end }} - serviceSelector: - matchLabels: - "io.kubernetes.service.namespace": {{ .Release.Namespace | quote }} - "gateway.networking.k8s.io/gateway-name": cozystack diff --git a/packages/extra/gateway/tests/gateway_test.yaml b/packages/extra/gateway/tests/gateway_test.yaml index 2490bf12..52c70ba5 100644 --- a/packages/extra/gateway/tests/gateway_test.yaml +++ b/packages/extra/gateway/tests/gateway_test.yaml @@ -1,7 +1,6 @@ suite: tenant gateway templates: - templates/gateway.yaml - - templates/cilium-lb-pool.yaml - templates/issuer.yaml release: @@ -9,7 +8,7 @@ release: namespace: tenant-root tests: - - it: renders Certificate, Gateway, redirect HTTPRoute and CiliumLoadBalancerIPPool when IPs and host are set + - it: renders Certificate, Gateway and redirect HTTPRoute when IPs and host are set set: _cluster: solver: dns01 @@ -64,67 +63,6 @@ tests: equal: path: spec.rules[0].filters[0].requestRedirect.scheme value: https - - template: templates/cilium-lb-pool.yaml - hasDocuments: - count: 1 - - template: templates/cilium-lb-pool.yaml - equal: - path: apiVersion - value: cilium.io/v2 - - template: templates/cilium-lb-pool.yaml - equal: - path: spec.blocks - value: - - cidr: 192.0.2.10/32 - - cidr: 192.0.2.11/32 - - template: templates/cilium-lb-pool.yaml - equal: - path: spec.serviceSelector.matchLabels["gateway.networking.k8s.io/gateway-name"] - value: cozystack - - - it: IPv6 external IPs use /128 CIDRs - set: - _cluster: - solver: dns01 - expose-ingress: tenant-root - expose-external-ips: "2001:db8::1,192.0.2.10" - _namespace: - host: example.org - asserts: - - template: templates/cilium-lb-pool.yaml - equal: - path: spec.blocks - value: - - cidr: 2001:db8::1/128 - - cidr: 192.0.2.10/32 - - - it: empty entries in expose-external-ips are filtered out - set: - _cluster: - solver: dns01 - expose-ingress: tenant-root - expose-external-ips: "192.0.2.10,," - _namespace: - host: example.org - asserts: - - template: templates/cilium-lb-pool.yaml - equal: - path: spec.blocks - value: - - cidr: 192.0.2.10/32 - - - it: chart render fails when publishing.externalIPs is empty so the operator never ships a silently non-working Gateway - set: - _cluster: - solver: dns01 - expose-ingress: tenant-root - expose-external-ips: "" - _namespace: - host: example.org - asserts: - - template: templates/cilium-lb-pool.yaml - failedTemplate: - errorMessage: "publishing.externalIPs is empty — a tenant Gateway has no CiliumLoadBalancerIPPool to draw from. The Gateway Service would stay in with no address. Set publishing.externalIPs on the platform (or publishing.exposure=loadBalancer with a BGP / L2-announce pool you manage separately) before enabling tenant.spec.gateway=true." - it: child tenant renders its own Gateway with the child's per-tenant apex set: @@ -163,10 +101,6 @@ tests: path: spec.listeners[0].allowedRoutes.namespaces.selector.matchExpressions[0].values value: - tenant-alice - - template: templates/cilium-lb-pool.yaml - equal: - path: spec.serviceSelector.matchLabels["io.kubernetes.service.namespace"] - value: tenant-alice - it: empty _namespace.host fails with explicit error set: From 75c06ce6e8467781376e1d7dc215ca2f70c09e53 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 26 Apr 2026 02:51:35 +0300 Subject: [PATCH 51/54] fix(tests): use dashless tenant name in tenant-host-policy probe The gateway.bats e2e test creates a Tenant named 'vap-host-probe' to verify that a non-trusted ServiceAccount cannot set tenant.spec.host. Tenant name validation rejects dashes (lowercase letters and digits only), so the apply was rejected by the name validator before reaching the cozystack-tenant-host-policy ValidatingAdmissionPolicy. Under the test's set -e, the subsequent grep for 'ValidatingAdmissionPolicy' in the error output then exits non-zero, failing the test. Renaming the probe to 'vaphostprobe' lets the request pass the name validator and reach the VAP, which is what the test is meant to exercise. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- hack/e2e-apps/gateway.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/e2e-apps/gateway.bats b/hack/e2e-apps/gateway.bats index 38d8803b..c97fc80a 100644 --- a/hack/e2e-apps/gateway.bats +++ b/hack/e2e-apps/gateway.bats @@ -186,7 +186,7 @@ EOF apiVersion: apps.cozystack.io/v1alpha1 kind: Tenant metadata: - name: vap-host-probe + name: vaphostprobe namespace: tenant-test spec: host: foreign.example.org @@ -197,10 +197,10 @@ EOF echo "$output" | grep -qi "ValidatingAdmissionPolicy" echo "$output" | grep -q "spec.host can only be set" else - kubectl -n tenant-test delete tenants.apps.cozystack.io vap-host-probe --ignore-not-found + kubectl -n tenant-test delete tenants.apps.cozystack.io vaphostprobe --ignore-not-found kubectl -n tenant-test delete rolebinding vap-probe-tenant-create --ignore-not-found kubectl -n tenant-test delete role vap-probe-tenant-create --ignore-not-found - echo "BUG: admission accepted tenant.spec.host from untrusted SA — Tenant 'vap-host-probe' was created" >&2 + echo "BUG: admission accepted tenant.spec.host from untrusted SA — Tenant 'vaphostprobe' was created" >&2 echo "$output" >&2 return 1 fi From 1146abce94ec2699f68f3aaffc58cdafbd9e432b Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 26 Apr 2026 04:35:19 +0300 Subject: [PATCH 52/54] fix(cozystack-api): invoke admission chain on Application Create and Delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom REST handler at pkg/registry/apps/application/rest.go is the storage backend for every kind in apps.cozystack.io/v1alpha1 (Tenant, MariaDB, Postgres, Kubernetes, ...). genericapiserver passes the admission chain — mutating webhooks, validating webhooks, and ValidatingAdmissionPolicies — to each storage method as a callback: createValidation for Create, updateValidation for Update, deleteValidation for Delete. Update was correctly invoking its callback since 23e399bd, but Create and Delete accepted the parameter and never called it. Admission was silently bypassed for both verbs on every apps.cozystack.io resource. The ValidatingAdmissionPolicy cozystack-tenant-host-policy added in this PR is the first in-tree policy that depends on Create-side enforcement. The accompanying e2e probe 'cozystack-tenant-host-policy blocks non-trusted callers from setting tenant.spec.host' caught the gap. Mirrors the existing updateValidation pattern: invoke createValidation in Create after the cheap format/length checks but before any state change, and invoke deleteValidation in Delete after the HelmRelease has been resolved (so admission sees the same shape it would on UPDATE) but before the actual r.c.Delete call. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/registry/apps/application/rest.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index 0728ea13..896f3eb7 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -181,6 +181,16 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation return nil, apierrors.NewBadRequest(err.Error()) } + // Run the genericapiserver-supplied admission chain (mutating + validating + // webhooks and ValidatingAdmissionPolicies) before persisting anything. + // Custom REST handlers must invoke this hook explicitly — unlike + // genericregistry.Store, which wires it automatically. + if createValidation != nil { + if err := createValidation(ctx, obj); err != nil { + return nil, err + } + } + // Convert Application to HelmRelease helmRelease, err := r.ConvertApplicationToHelmRelease(app) if err != nil { @@ -590,6 +600,22 @@ func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.Va return nil, false, apierrors.NewNotFound(r.gvr.GroupResource(), name) } + // Run the genericapiserver-supplied admission chain (validating webhooks + // and ValidatingAdmissionPolicies) on the resolved Application before + // removing the underlying HelmRelease. Custom REST handlers must invoke + // this hook explicitly — unlike genericregistry.Store, which wires it + // automatically. + if deleteValidation != nil { + converted, convErr := r.ConvertHelmReleaseToApplication(ctx, helmRelease) + if convErr != nil { + klog.Errorf("Failed to convert HelmRelease %s to Application for delete admission: %v", helmReleaseName, convErr) + return nil, false, convErr + } + if err := deleteValidation(ctx, &converted); err != nil { + return nil, false, err + } + } + klog.V(6).Infof("Deleting HelmRelease %s in namespace %s", helmReleaseName, namespace) // Delete the HelmRelease corresponding to the Application