diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index f4fb77abe05..8e7482b042d 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -639,6 +639,10 @@ func (s *Server) doServe() error { s.log.WithError(err).WithField("resource", "secrets").Fatal("failed to create informer") } + if err := s.informOnResource(&corev1.ConfigMap{}, handler); err != nil { + s.log.WithError(err).WithField("resource", "configmaps").Fatal("failed to create informer") + } + // Inform on endpoints/endpointSlices. if contourConfiguration.FeatureFlags.IsEndpointSliceEnabled() { if err := s.informOnResource(&discoveryv1.EndpointSlice{}, &contour.EventRecorder{ @@ -1053,9 +1057,10 @@ func (s *Server) setupGatewayAPI(contourConfiguration contour_api_v1alpha1.Conto // Some features may be disabled. features := map[string]struct{}{ - "tlsroutes": {}, - "grpcroutes": {}, - "tcproutes": {}, + "tlsroutes": {}, + "grpcroutes": {}, + "tcproutes": {}, + "backendtlspolicy": {}, } for _, f := range s.ctx.disabledFeatures { delete(features, f) @@ -1087,6 +1092,13 @@ func (s *Server) setupGatewayAPI(contourConfiguration contour_api_v1alpha1.Conto } } + // Create and register the TCPRoute controller with the manager. + if _, enabled := features["backendtlspolicy"]; enabled { + if err := controller.RegisterBackendTLSPolicyController(s.log.WithField("context", "backendtlspolicy-controller"), mgr, eventHandler); err != nil { + s.log.WithError(err).Fatal("failed to create backendtlspolicy-controller") + } + } + // Inform on ReferenceGrants. if err := s.informOnResource(&gatewayapi_v1beta1.ReferenceGrant{}, eventHandler); err != nil { s.log.WithError(err).WithField("resource", "referencegrants").Fatal("failed to create informer") diff --git a/examples/contour/02-role-contour.yaml b/examples/contour/02-role-contour.yaml index 35e9386e719..21bf5738142 100644 --- a/examples/contour/02-role-contour.yaml +++ b/examples/contour/02-role-contour.yaml @@ -10,6 +10,7 @@ rules: - apiGroups: - "" resources: + - configmaps - endpoints - namespaces - secrets @@ -29,6 +30,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies - gatewayclasses - gateways - grpcroutes @@ -43,6 +45,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies/status - gatewayclasses/status - gateways/status - grpcroutes/status diff --git a/examples/gateway-provisioner/01-roles.yaml b/examples/gateway-provisioner/01-roles.yaml index b43bba73e28..9726f1fd006 100644 --- a/examples/gateway-provisioner/01-roles.yaml +++ b/examples/gateway-provisioner/01-roles.yaml @@ -10,6 +10,7 @@ rules: - apiGroups: - "" resources: + - configmaps - endpoints - namespaces - secrets @@ -70,15 +71,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: - - gatewayclasses - - gateways - verbs: - - get - - list - - watch -- apiGroups: - - gateway.networking.k8s.io - resources: + - backendtlspolicies - gatewayclasses - gateways - grpcroutes @@ -93,19 +86,29 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies/status - gatewayclasses/status - gateways/status + - grpcroutes/status + - httproutes/status + - tcproutes/status + - tlsroutes/status verbs: - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + - gateways + verbs: + - get + - list + - watch - apiGroups: - gateway.networking.k8s.io resources: - gatewayclasses/status - gateways/status - - grpcroutes/status - - httproutes/status - - tcproutes/status - - tlsroutes/status verbs: - update - apiGroups: diff --git a/examples/gateway/00-crds.yaml b/examples/gateway/00-crds.yaml index f483bcb0ac6..145128037c1 100644 --- a/examples/gateway/00-crds.yaml +++ b/examples/gateway/00-crds.yaml @@ -1,5 +1,487 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/2466 + gateway.networking.k8s.io/bundle-version: v1.0.0 + gateway.networking.k8s.io/channel: experimental + creationTimestamp: null + labels: + gateway.networking.k8s.io/policy: Direct + name: backendtlspolicies.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: BackendTLSPolicy + listKind: BackendTLSPolicyList + plural: backendtlspolicies + shortNames: + - btlspolicy + singular: backendtlspolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + 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: + targetRef: + description: "TargetRef 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. \n Support: Extended for Kubernetes Service + \n Support: Implementation-specific for any other resource" + 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 + namespace: + description: Namespace is the namespace of the referent. When + unspecified, the local namespace is inferred. Even when policy + targets a resource in a different namespace, it MUST only apply + to traffic originating from the same namespace as the policy. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + 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: \n * Gateway: Listener Name * + Service: Port Name \n 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 + tls: + description: TLS contains backend TLS policy configuration. + properties: + caCertRefs: + description: "CACertRefs 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. \n If CACertRefs is empty or unspecified, then + WellKnownCACerts must be specified. Only one of CACertRefs or + WellKnownCACerts may be specified, not both. If CACertRefs is + empty or unspecified, the configuration for WellKnownCACerts + MUST be honored instead. \n References to a resource in a different + namespace are invalid for the moment, although we will revisit + this in the future. \n A single CACertRef 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. \n Support: Core - An optional single + reference to a Kubernetes ConfigMap, with the CA certificate + in a key named `ca.crt`. \n Support: Implementation-specific + (More than one reference, or other kinds of resources)." + 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. \n 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 + hostname: + description: "Hostname is used for two purposes in the connection + between Gateways and backends: \n 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. \n 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 + wellKnownCACerts: + description: "WellKnownCACerts specifies whether system CA certificates + may be used in the TLS handshake between the gateway and backend + pod. \n If WellKnownCACerts is unspecified or empty (\"\"), + then CACertRefs must be specified with at least one entry for + a valid configuration. Only one of CACertRefs or WellKnownCACerts + may be specified, not both. \n Support: Core for \"System\"" + enum: + - System + type: string + required: + - hostname + type: object + x-kubernetes-validations: + - message: must not contain both CACertRefs and WellKnownCACerts + rule: '!(has(self.caCertRefs) && size(self.caCertRefs) > 0 && has(self.wellKnownCACerts) + && self.wellKnownCACerts != "")' + - message: must specify either CACertRefs or WellKnownCACerts + rule: (has(self.caCertRefs) && size(self.caCertRefs) > 0 || has(self.wellKnownCACerts) + && self.wellKnownCACerts != "") + required: + - targetRef + - tls + 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. \n 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. \n 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. \n 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. \n A maximum of 16 ancestors will be represented + in this list. An empty list means the Policy is not relevant for + any ancestors. \n 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. \n 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. \n 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. \n 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. \n 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. \n 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. \n 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). \n + 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. \n There are + two kinds of parent resources with \"Core\" support: \n + * Gateway (Gateway conformance profile) * Service (Mesh + conformance profile, experimental, ClusterIP Services + only) \n 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. \n 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. \n 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. \n 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. \n 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. \n 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. \n 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. \n 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. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n 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. + \n Support: Extended \n " + 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: \n * 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. Note that attaching Routes to Services + as Parents is part of experimental Mesh support and is + not supported for any other purpose. \n Implementations + MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n 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. + \n 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. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + 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. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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. \n Example: + \"example.net/gateway-controller\". \n 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). + \n 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/2466 @@ -1076,6 +1558,9 @@ spec: '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: hostname must not be specified for protocols ['TCP', 'UDP'] rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) || l.hostname == '''') : true)' @@ -1964,6 +2449,9 @@ spec: '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: hostname must not be specified for protocols ['TCP', 'UDP'] rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) || l.hostname == '''') : true)' diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index bd408dd93ee..e7b9c93b9e8 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -8839,6 +8839,7 @@ rules: - apiGroups: - "" resources: + - configmaps - endpoints - namespaces - secrets @@ -8858,6 +8859,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies - gatewayclasses - gateways - grpcroutes @@ -8872,6 +8874,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies/status - gatewayclasses/status - gateways/status - grpcroutes/status diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 518e9803a5e..a3901be5a22 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -19779,6 +19779,7 @@ rules: - apiGroups: - "" resources: + - configmaps - endpoints - namespaces - secrets @@ -19839,15 +19840,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: - - gatewayclasses - - gateways - verbs: - - get - - list - - watch -- apiGroups: - - gateway.networking.k8s.io - resources: + - backendtlspolicies - gatewayclasses - gateways - grpcroutes @@ -19862,19 +19855,29 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies/status - gatewayclasses/status - gateways/status + - grpcroutes/status + - httproutes/status + - tcproutes/status + - tlsroutes/status verbs: - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + - gateways + verbs: + - get + - list + - watch - apiGroups: - gateway.networking.k8s.io resources: - gatewayclasses/status - gateways/status - - grpcroutes/status - - httproutes/status - - tcproutes/status - - tlsroutes/status verbs: - update - apiGroups: diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index 80b01219c9c..3b26e56e6f8 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -8842,6 +8842,7 @@ rules: - apiGroups: - "" resources: + - configmaps - endpoints - namespaces - secrets @@ -8861,6 +8862,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies - gatewayclasses - gateways - grpcroutes @@ -8875,6 +8877,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies/status - gatewayclasses/status - gateways/status - grpcroutes/status diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index f12b0fe13f1..250d62badf9 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -8839,6 +8839,7 @@ rules: - apiGroups: - "" resources: + - configmaps - endpoints - namespaces - secrets @@ -8858,6 +8859,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies - gatewayclasses - gateways - grpcroutes @@ -8872,6 +8874,7 @@ rules: - apiGroups: - gateway.networking.k8s.io resources: + - backendtlspolicies/status - gatewayclasses/status - gateways/status - grpcroutes/status diff --git a/internal/controller/backendtlspolicy.go b/internal/controller/backendtlspolicy.go new file mode 100644 index 00000000000..6891b3b9811 --- /dev/null +++ b/internal/controller/backendtlspolicy.go @@ -0,0 +1,77 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +type backendTLSPolicyReconciler struct { + client client.Client + eventHandler cache.ResourceEventHandler + logrus.FieldLogger +} + +// RegisterBackendTLSPolicyController creates the backendtlspolicy controller from mgr. The controller will be pre-configured +// to watch for BackendTLSPolicy objects across all namespaces. +func RegisterBackendTLSPolicyController(log logrus.FieldLogger, mgr manager.Manager, eventHandler cache.ResourceEventHandler) error { + r := &backendTLSPolicyReconciler{ + client: mgr.GetClient(), + eventHandler: eventHandler, + FieldLogger: log, + } + c, err := controller.NewUnmanaged("backendtlspolicy-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + if err := mgr.Add(&noLeaderElectionController{c}); err != nil { + return err + } + + return c.Watch(source.Kind(mgr.GetCache(), &gatewayapi_v1alpha2.BackendTLSPolicy{}), &handler.EnqueueRequestForObject{}) +} + +func (r *backendTLSPolicyReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + + // Fetch the BackendTLSPolicy from the cache. + backendTLSPolicy := &gatewayapi_v1alpha2.BackendTLSPolicy{} + err := r.client.Get(ctx, request.NamespacedName, backendTLSPolicy) + if errors.IsNotFound(err) { + r.eventHandler.OnDelete(&gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: request.Name, + Namespace: request.Namespace, + }, + }) + return reconcile.Result{}, nil + } + + // Pass the new changed object off to the eventHandler. + r.eventHandler.OnAdd(backendTLSPolicy, false) + + return reconcile.Result{}, nil +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 68e1bcb030a..2169047989c 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -45,6 +45,9 @@ func TestRegisterControllers(t *testing.T) { "grpcroute controller": func(mockManager *mocks.Manager) error { return controller.RegisterGRPCRouteController(fixture.NewTestLogger(t), mockManager, nil) }, + "backendtlspolicy controller": func(mockManager *mocks.Manager) error { + return controller.RegisterBackendTLSPolicyController(fixture.NewTestLogger(t), mockManager, nil) + }, } for name, test := range tests { diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index 5709a3d9450..81b0cdeeaa5 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -31,6 +31,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" gatewayapi_v1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -87,6 +88,29 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, } + tlsService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlssvc", + Namespace: "projectcontour", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{makeServicePort("https", "TCP", 443, 8443)}, + }, + } + + tlsAndNonTLSService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsandnontlssvc", + Namespace: "projectcontour", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + makeServicePort("http", "TCP", 80, 8080), + makeServicePort("https", "TCP", 443, 8443), + }, + }, + } + validClass := &gatewayapi_v1beta1.GatewayClass{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ @@ -312,6 +336,37 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, } + cert1 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "projectcontour", + }, + Type: v1.SecretTypeOpaque, + Data: map[string][]byte{ + CACertificateKey: []byte(fixture.CERTIFICATE), + }, + } + cert2 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca2", + Namespace: "projectcontour", + }, + Type: v1.SecretTypeOpaque, + Data: map[string][]byte{ + CACertificateKey: []byte(fixture.EC_CERTIFICATE), + }, + } + + configMapCert1 := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "projectcontour", + }, + Data: map[string]string{ + CACertificateKey: fixture.CERTIFICATE, + }, + } + sec1 := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret", @@ -475,6 +530,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { objs []any gatewayclass *gatewayapi_v1beta1.GatewayClass gateway *gatewayapi_v1beta1.Gateway + upstreamTLS *UpstreamTLS want []*Listener }{ "insert basic single route, single hostname": { @@ -4216,6 +4272,386 @@ func TestDAGInsertGatewayAPI(t *testing.T) { want: listeners(), }, + "HTTPRoute with BackendTLSPolicy": { + gatewayclass: validClass, + gateway: gatewayHTTPAllNamespaces, + upstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + objs: []any{ + tlsService, + configMapCert1, + &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")}, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/"), + BackendRefs: gatewayapi.HTTPBackendRefs( + gatewayapi.HTTPBackendRef("tlssvc", 443, 1), + ), + }}, + }, + }, + &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "tlssvc", + }, + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1alpha2.LocalObjectReference{{ + Kind: "ConfigMap", + Name: gatewayapi_v1.ObjectName(configMapCert1.Name), + }}, + Hostname: "example.com", + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("*", routeCluster("/", + &Cluster{ + Weight: 1, + Upstream: &Service{ + Protocol: "tls", + Weighted: WeightedService{ + Weight: 1, + ServiceName: tlsService.Name, + ServiceNamespace: tlsService.Namespace, + ServicePort: tlsService.Spec.Ports[0], + HealthPort: tlsService.Spec.Ports[0], + }, + }, + Protocol: "tls", + UpstreamValidation: &PeerValidationContext{ + CACertificates: []*Secret{ + caSecret(cert1), + }, + SubjectNames: []string{"example.com"}, + }, + UpstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + }, + )), + ), + }, + ), + }, + "HTTPRoute with BackendTLSPolicy and CA cert in secret": { + gatewayclass: validClass, + gateway: gatewayHTTPAllNamespaces, + upstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + objs: []any{ + tlsService, + cert1, + &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")}, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/"), + BackendRefs: gatewayapi.HTTPBackendRefs( + gatewayapi.HTTPBackendRef("tlssvc", 443, 1), + ), + }}, + }, + }, + &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "tlssvc", + }, + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1alpha2.LocalObjectReference{{ + Kind: "Secret", + Name: gatewayapi_v1.ObjectName(cert1.Name), + }}, + Hostname: "example.com", + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("*", routeCluster("/", + &Cluster{ + Weight: 1, + Upstream: &Service{ + Protocol: "tls", + Weighted: WeightedService{ + Weight: 1, + ServiceName: tlsService.Name, + ServiceNamespace: tlsService.Namespace, + ServicePort: tlsService.Spec.Ports[0], + HealthPort: tlsService.Spec.Ports[0], + }, + }, + Protocol: "tls", + UpstreamValidation: &PeerValidationContext{ + CACertificates: []*Secret{ + caSecret(cert1), + }, + SubjectNames: []string{"example.com"}, + }, + UpstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + }, + )), + ), + }, + ), + }, + "HTTPRoute with BackendTLSPolicy using multiple certs": { + gatewayclass: validClass, + gateway: gatewayHTTPAllNamespaces, + upstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + objs: []any{ + tlsService, + cert1, + cert2, + &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")}, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/"), + BackendRefs: gatewayapi.HTTPBackendRefs( + gatewayapi.HTTPBackendRef("tlssvc", 443, 1), + ), + }}, + }, + }, + &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "tlssvc", + }, + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1alpha2.LocalObjectReference{ + { + Kind: "Secret", + Name: gatewayapi_v1.ObjectName(cert1.Name), + }, + { + Kind: "Secret", + Name: gatewayapi_v1.ObjectName(cert2.Name), + }, + }, + Hostname: "example.com", + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("*", routeCluster("/", + &Cluster{ + Weight: 1, + Upstream: &Service{ + Protocol: "tls", + Weighted: WeightedService{ + Weight: 1, + ServiceName: tlsService.Name, + ServiceNamespace: tlsService.Namespace, + ServicePort: tlsService.Spec.Ports[0], + HealthPort: tlsService.Spec.Ports[0], + }, + }, + Protocol: "tls", + UpstreamValidation: &PeerValidationContext{ + CACertificates: []*Secret{ + caSecret(cert1), + caSecret(cert2), + }, + SubjectNames: []string{"example.com"}, + }, + UpstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + }, + )), + ), + }, + ), + }, + "HTTPRoute with BackendTLSPolicy and sectionName set": { + gatewayclass: validClass, + gateway: gatewayHTTPAllNamespaces, + upstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + objs: []any{ + tlsAndNonTLSService, + cert1, + &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")}, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/tls"), + BackendRefs: gatewayapi.HTTPBackendRefs( + gatewayapi.HTTPBackendRef("tlsandnontlssvc", 443, 1), + ), + }}, + }, + }, + &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-tls-basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")}, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/non-tls"), + BackendRefs: gatewayapi.HTTPBackendRefs( + gatewayapi.HTTPBackendRef("tlsandnontlssvc", 80, 1), + ), + }}, + }, + }, + &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "tlsandnontlssvc", + }, + SectionName: ptr.To(gatewayapi_v1.SectionName("https")), + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1alpha2.LocalObjectReference{{ + Kind: "Secret", + Name: gatewayapi_v1.ObjectName(cert1.Name), + }}, + Hostname: "example.com", + }, + }, + }, + }, + want: listeners( + &Listener{ + Name: "http-80", + VirtualHosts: virtualhosts( + virtualhost("*", + &Route{ + PathMatchCondition: prefixSegment("/tls"), + Clusters: []*Cluster{ + { + Weight: 1, + Upstream: &Service{ + Protocol: "tls", + Weighted: WeightedService{ + Weight: 1, + ServiceName: tlsAndNonTLSService.Name, + ServiceNamespace: tlsAndNonTLSService.Namespace, + ServicePort: tlsAndNonTLSService.Spec.Ports[1], + HealthPort: tlsAndNonTLSService.Spec.Ports[1], + }, + }, + Protocol: "tls", + UpstreamValidation: &PeerValidationContext{ + CACertificates: []*Secret{ + caSecret(cert1), + }, + SubjectNames: []string{"example.com"}, + }, + UpstreamTLS: &UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }, + }, + }, + }, + &Route{ + PathMatchCondition: prefixSegment("/non-tls"), + Clusters: []*Cluster{ + { + Weight: 1, + Upstream: &Service{ + Weighted: WeightedService{ + Weight: 1, + ServiceName: tlsAndNonTLSService.Name, + ServiceNamespace: tlsAndNonTLSService.Namespace, + ServicePort: tlsAndNonTLSService.Spec.Ports[0], + HealthPort: tlsAndNonTLSService.Spec.Ports[0], + }, + }, + }, + }, + }, + ), + ), + }, + ), + }, "different weights for multiple forwardTos": { gatewayclass: validClass, gateway: gatewayHTTPAllNamespaces, @@ -5906,6 +6342,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, &GatewayAPIProcessor{ FieldLogger: fixture.NewTestLogger(t), + UpstreamTLS: tc.upstreamTLS, }, }, } @@ -11359,8 +11796,10 @@ func TestDAGInsert(t *testing.T) { }, Protocol: "tls", UpstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), - SubjectNames: []string{"example.com"}, + CACertificates: []*Secret{ + caSecret(cert1), + }, + SubjectNames: []string{"example.com"}, }, }, ), @@ -11392,8 +11831,10 @@ func TestDAGInsert(t *testing.T) { }, Protocol: "h2", UpstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), - SubjectNames: []string{"example.com"}, + CACertificates: []*Secret{ + caSecret(cert1), + }, + SubjectNames: []string{"example.com"}, }, }, ), @@ -11467,8 +11908,10 @@ func TestDAGInsert(t *testing.T) { }, Protocol: "tls", UpstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert2), - SubjectNames: []string{"example.com"}, + CACertificates: []*Secret{ + caSecret(cert2), + }, + SubjectNames: []string{"example.com"}, }, }, ), @@ -11503,7 +11946,9 @@ func TestDAGInsert(t *testing.T) { MaxTLSVersion: "1.3", Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), + CACertificates: []*Secret{ + caSecret(cert1), + }, }, }, ), @@ -11533,7 +11978,9 @@ func TestDAGInsert(t *testing.T) { MaxTLSVersion: "1.3", Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), + CACertificates: []*Secret{ + caSecret(cert1), + }, }, }, ), @@ -11600,7 +12047,9 @@ func TestDAGInsert(t *testing.T) { Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ SkipClientCertValidation: true, - CACertificate: caSecret(cert1), + CACertificates: []*Secret{ + caSecret(cert1), + }, }, }, ), @@ -11633,8 +12082,10 @@ func TestDAGInsert(t *testing.T) { MaxTLSVersion: "1.3", Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), - CRL: crlSecret(crl), + CACertificates: []*Secret{ + caSecret(cert1), + }, + CRL: crlSecret(crl), }, }, ), @@ -11667,7 +12118,9 @@ func TestDAGInsert(t *testing.T) { MaxTLSVersion: "1.3", Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), + CACertificates: []*Secret{ + caSecret(cert1), + }, CRL: crlSecret(crl), OnlyVerifyLeafCertCrl: true, }, @@ -11702,7 +12155,9 @@ func TestDAGInsert(t *testing.T) { MaxTLSVersion: "1.3", Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), + CACertificates: []*Secret{ + caSecret(cert1), + }, ForwardClientCertificate: &ClientCertificateDetails{ Subject: true, Cert: true, @@ -11742,7 +12197,9 @@ func TestDAGInsert(t *testing.T) { MaxTLSVersion: "1.3", Secret: secret(sec1), DownstreamValidation: &PeerValidationContext{ - CACertificate: caSecret(cert1), + CACertificates: []*Secret{ + caSecret(cert1), + }, OptionalClientCertificate: true, }, }, diff --git a/internal/dag/cache.go b/internal/dag/cache.go index 21f2bdaa7a4..fdbbaf0f679 100644 --- a/internal/dag/cache.go +++ b/internal/dag/cache.go @@ -63,6 +63,7 @@ type KubernetesCache struct { ingresses map[types.NamespacedName]*networking_v1.Ingress httpproxies map[types.NamespacedName]*contour_api_v1.HTTPProxy secrets map[types.NamespacedName]*Secret + configmapsecrets map[types.NamespacedName]*Secret tlscertificatedelegations map[types.NamespacedName]*contour_api_v1.TLSCertificateDelegation services map[types.NamespacedName]*v1.Service namespaces map[string]*v1.Namespace @@ -73,6 +74,7 @@ type KubernetesCache struct { grpcroutes map[types.NamespacedName]*gatewayapi_v1alpha2.GRPCRoute tcproutes map[types.NamespacedName]*gatewayapi_v1alpha2.TCPRoute referencegrants map[types.NamespacedName]*gatewayapi_v1beta1.ReferenceGrant + backendtlspolicies map[types.NamespacedName]*gatewayapi_v1alpha2.BackendTLSPolicy extensions map[types.NamespacedName]*contour_api_v1alpha1.ExtensionService // Metrics contains Prometheus metrics. @@ -99,6 +101,7 @@ func (kc *KubernetesCache) init() { kc.ingresses = make(map[types.NamespacedName]*networking_v1.Ingress) kc.httpproxies = make(map[types.NamespacedName]*contour_api_v1.HTTPProxy) kc.secrets = make(map[types.NamespacedName]*Secret) + kc.configmapsecrets = make(map[types.NamespacedName]*Secret) kc.tlscertificatedelegations = make(map[types.NamespacedName]*contour_api_v1.TLSCertificateDelegation) kc.services = make(map[types.NamespacedName]*v1.Service) kc.namespaces = make(map[string]*v1.Namespace) @@ -107,6 +110,7 @@ func (kc *KubernetesCache) init() { kc.tlsroutes = make(map[types.NamespacedName]*gatewayapi_v1alpha2.TLSRoute) kc.grpcroutes = make(map[types.NamespacedName]*gatewayapi_v1alpha2.GRPCRoute) kc.tcproutes = make(map[types.NamespacedName]*gatewayapi_v1alpha2.TCPRoute) + kc.backendtlspolicies = make(map[types.NamespacedName]*gatewayapi_v1alpha2.BackendTLSPolicy) kc.extensions = make(map[types.NamespacedName]*contour_api_v1alpha1.ExtensionService) } @@ -125,6 +129,15 @@ func (kc *KubernetesCache) Insert(obj any) bool { kc.secrets[k8s.NamespacedNameOf(obj)] = &Secret{Object: obj} return kc.secretTriggersRebuild(obj), len(kc.secrets) + case *v1.ConfigMap: + // Only insert configmaps that are CA certs, i.e has 'ca.crt' key, + // into cache. + if secret, isCA := kc.convertCACertConfigMapToSecret(obj); isCA { + kc.configmapsecrets[k8s.NamespacedNameOf(obj)] = &Secret{Object: secret} + return kc.configMapTriggersRebuild(obj), len(kc.configmapsecrets) + } + return false, len(kc.configmapsecrets) + case *v1.Service: kc.services[k8s.NamespacedNameOf(obj)] = obj return kc.serviceTriggersRebuild(obj), len(kc.services) @@ -236,6 +249,10 @@ func (kc *KubernetesCache) Insert(obj any) bool { kc.referencegrants[k8s.NamespacedNameOf(obj)] = obj return true, len(kc.referencegrants) + case *gatewayapi_v1alpha2.BackendTLSPolicy: + kc.backendtlspolicies[k8s.NamespacedNameOf(obj)] = obj + return true, len(kc.backendtlspolicies) + case *contour_api_v1alpha1.ExtensionService: kc.extensions[k8s.NamespacedNameOf(obj)] = obj return true, len(kc.extensions) @@ -303,6 +320,11 @@ func (kc *KubernetesCache) remove(obj any) (bool, int) { delete(kc.secrets, m) return kc.secretTriggersRebuild(obj), len(kc.secrets) + case *v1.ConfigMap: + m := k8s.NamespacedNameOf(obj) + delete(kc.configmapsecrets, m) + return kc.configMapTriggersRebuild(obj), len(kc.configmapsecrets) + case *v1.Service: m := k8s.NamespacedNameOf(obj) delete(kc.services, m) @@ -389,6 +411,12 @@ func (kc *KubernetesCache) remove(obj any) (bool, int) { delete(kc.referencegrants, m) return ok, len(kc.referencegrants) + case *gatewayapi_v1alpha2.BackendTLSPolicy: + m := k8s.NamespacedNameOf(obj) + _, ok := kc.backendtlspolicies[m] + delete(kc.backendtlspolicies, m) + return ok, len(kc.backendtlspolicies) + case *contour_api_v1alpha1.ExtensionService: m := k8s.NamespacedNameOf(obj) _, ok := kc.extensions[m] @@ -579,6 +607,32 @@ func isRefToSecret(ref gatewayapi_v1beta1.SecretObjectReference, secret *v1.Secr string(ref.Name) == secret.Name } +// configMapTriggersRebuild returns true if this configmap is referenced by a +// BackendTLSPolicy object. +func (kc *KubernetesCache) configMapTriggersRebuild(configMapObj *v1.ConfigMap) bool { + configMap := types.NamespacedName{ + Namespace: configMapObj.Namespace, + Name: configMapObj.Name, + } + + for _, backendtlspolicy := range kc.backendtlspolicies { + for _, caCertRef := range backendtlspolicy.Spec.TLS.CACertRefs { + if caCertRef.Group != "" || caCertRef.Kind != "ConfigMap" { + continue + } + + caCertRefNamespacedName := types.NamespacedName{ + Namespace: backendtlspolicy.Namespace, + Name: string(caCertRef.Name), + } + if configMap == caCertRefNamespacedName { + return true + } + } + } + return false +} + // routeTriggersRebuild returns true if this route references gateway in this cache. func (kc *KubernetesCache) routeTriggersRebuild(parentRefs []gatewayapi_v1beta1.ParentReference) bool { if kc.gateway == nil { @@ -631,6 +685,27 @@ func (kc *KubernetesCache) LookupCASecret(name types.NamespacedName, targetNames return sec, nil } +// LookupCAConfigMap returns ConfigMap converted into dag.Secret with CA certificate from cache. +func (kc *KubernetesCache) LookupCAConfigMap(name types.NamespacedName) (*Secret, error) { + sec, ok := kc.configmapsecrets[name] + if !ok { + return nil, fmt.Errorf("ConfigMap not found") + } + + // Compute and store the validation result if not + // already stored. + if sec.ValidCASecret == nil { + sec.ValidCASecret = &SecretValidationStatus{ + Error: validCASecret(sec.Object), + } + } + + if err := sec.ValidCASecret.Error; err != nil { + return nil, err + } + return sec, nil +} + // LookupCRLSecret returns Secret with CRL from the cache. // If name (referred Secret) is in different namespace than targetNamespace (the referring object), // then delegation check is performed. @@ -676,7 +751,9 @@ func (kc *KubernetesCache) LookupUpstreamValidation(uv *contour_api_v1.UpstreamV } return nil, fmt.Errorf("invalid CA Secret %q: %s", caCertificate, err) } - pvc.CACertificate = cacert + pvc.CACertificates = []*Secret{ + cacert, + } // CEL validation should enforce that SubjectName must be set if SubjectNames is used. So, SubjectName will always be present. if uv.SubjectName == "" { @@ -777,3 +854,57 @@ func (kc *KubernetesCache) LookupService(meta types.NamespacedName, port intstr. return nil, v1.ServicePort{}, fmt.Errorf("port %q on service %q not matched", port.String(), meta) } + +// LookupBackendTLSPolicyByTargetRef returns the Kubernetes BackendTLSPolicies that matches the provided targetRef with +// a SectionName, if possible. A BackendTLSPolicy may be returned if there is a BackendTLSPolicy matching the targetRef +// but has no SectionName. +// +// For example, there could be two BackendTLSPolicies matching Service "foo". One of them matches SectionName "https", +// but the other has no SectionName and functions as a catch-all policy for service "foo". +// +// If a policy is found, true is returned. +func (kc *KubernetesCache) LookupBackendTLSPolicyByTargetRef(targetRef gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName) (*gatewayapi_v1alpha2.BackendTLSPolicy, bool) { + var fallbackBackendTLSPolicy *gatewayapi_v1alpha2.BackendTLSPolicy + for _, v := range kc.backendtlspolicies { + namespaceMatches := v.Spec.TargetRef.PolicyTargetReference.Namespace == nil && targetRef.PolicyTargetReference.Namespace == nil || + v.Spec.TargetRef.PolicyTargetReference.Namespace != nil && targetRef.PolicyTargetReference.Namespace != nil && + *v.Spec.TargetRef.PolicyTargetReference.Namespace == *targetRef.PolicyTargetReference.Namespace + + sectionNameMatches := v.Spec.TargetRef.SectionName == nil && targetRef.SectionName == nil || + v.Spec.TargetRef.SectionName != nil && targetRef.SectionName != nil && + *v.Spec.TargetRef.SectionName == *targetRef.SectionName + + if v.Spec.TargetRef.PolicyTargetReference.Group == targetRef.Group && + v.Spec.TargetRef.PolicyTargetReference.Kind == targetRef.Kind && + v.Spec.TargetRef.PolicyTargetReference.Name == targetRef.Name && + namespaceMatches { + if sectionNameMatches { + return v, true + } + + if v.Spec.TargetRef.SectionName == nil { + fallbackBackendTLSPolicy = v + } + } + } + + if fallbackBackendTLSPolicy != nil { + return fallbackBackendTLSPolicy, true + } + + return nil, false +} + +func (kc *KubernetesCache) convertCACertConfigMapToSecret(configMap *v1.ConfigMap) (*v1.Secret, bool) { + if _, ok := configMap.Data[CACertificateKey]; !ok { + return nil, false + } + + return &v1.Secret{ + ObjectMeta: configMap.ObjectMeta, + Data: map[string][]byte{ + CACertificateKey: []byte(configMap.Data[CACertificateKey]), + }, + Type: v1.SecretTypeOpaque, + }, true +} diff --git a/internal/dag/cache_test.go b/internal/dag/cache_test.go index 61f6cf74741..d4e9ce83cb4 100644 --- a/internal/dag/cache_test.go +++ b/internal/dag/cache_test.go @@ -31,8 +31,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -361,6 +363,60 @@ func TestKubernetesCacheInsert(t *testing.T) { }, want: true, }, + "insert certificate configmap not referenced": { + obj: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + CACertificateKey: fixture.CERTIFICATE, + }, + }, + want: false, + }, + "insert generic configmap not referenced": { + obj: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + "not-ca.crt": fixture.CERTIFICATE, + }, + }, + want: false, + }, + "insert certificate configmap referenced by BackendTLSPolicy": { + pre: []any{ + &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-btp", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1alpha2.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "ca", + }, + }, + }, + }, + }, + }, + obj: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + CACertificateKey: fixture.CERTIFICATE, + }, + }, + want: true, + }, "insert ingressv1 empty ingress class": { obj: &networking_v1.Ingress{ ObjectMeta: metav1.ObjectMeta{ @@ -1035,6 +1091,48 @@ func TestKubernetesCacheInsert(t *testing.T) { }, want: true, }, + "insert backendtlspolicy referenced by gateway-api HTTPRoute": { + pre: []any{ + &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "httproute", + Namespace: "default", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1alpha2.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1alpha2.ParentReference{ + gatewayapi.GatewayParentRef("projectcontour", "contour"), + }, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + BackendRefs: gatewayapi.HTTPBackendRef("service", 80, 1), + }}, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service", + Namespace: "default", + }, + }, + }, + obj: &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backendtlspolicy", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "service", + }, + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{}, + }, + }, + want: true, + }, // SPECIFIC GATEWAY TESTS "specific gateway configured, insert gatewayclass, no gateway cached": { @@ -1195,6 +1293,24 @@ func TestKubernetesCacheRemove(t *testing.T) { }, want: false, }, + "remove configmap": { + cache: cache(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "default", + }, + Data: map[string]string{ + CACertificateKey: fixture.CERTIFICATE, + }, + }), + obj: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "default", + }, + }, + want: false, + }, "remove service": { cache: cache(&v1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -1655,6 +1771,57 @@ func TestKubernetesCacheRemove(t *testing.T) { }, want: true, }, + "remove gateway-api BackendTLSPolicy": { + cache: cache(&gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backendtlspolicy", + Namespace: "default", + }, + }), + obj: &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backendtlspolicy", + Namespace: "default", + }, + }, + want: true, + }, + "remove configmap that is referenced by gateway-api BackendTLSPolicy": { + cache: cache( + &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backendtlspolicy", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []v1beta1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + }, + }, + }, + }, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "default", + }, + Data: map[string]string{ + CACertificateKey: fixture.CERTIFICATE, + }, + }, + ), + obj: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "default", + }, + }, + want: true, + }, "remove extension service": { cache: cache(&contour_api_v1alpha1.ExtensionService{ ObjectMeta: fixture.ObjectMeta("default/extension"), @@ -2693,9 +2860,11 @@ func TestLookupUpstreamValidation(t *testing.T) { pvc := func(subjectNames []string) *PeerValidationContext { return &PeerValidationContext{ - CACertificate: &Secret{ - Object: secret(), - ValidCASecret: &SecretValidationStatus{}, + CACertificates: []*Secret{ + { + Object: secret(), + ValidCASecret: &SecretValidationStatus{}, + }, }, SubjectNames: subjectNames, } @@ -2751,3 +2920,235 @@ func TestLookupUpstreamValidation(t *testing.T) { }) } } + +func TestLookupBackendTLSPolicyByTargetRef(t *testing.T) { + targetRef := func(group, kind, name string, namespace, sectionName *string) gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName { + var ns *gatewayapi_v1alpha2.Namespace + if namespace != nil { + ns = ptr.To(gatewayapi_v1alpha2.Namespace(*namespace)) + } + var sn *gatewayapi_v1alpha2.SectionName + if sectionName != nil { + sn = ptr.To(gatewayapi_v1alpha2.SectionName(*sectionName)) + } + return gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Group: gatewayapi_v1alpha2.Group(group), + Kind: gatewayapi_v1alpha2.Kind(kind), + Name: gatewayapi_v1alpha2.ObjectName(name), + Namespace: ns, + }, + SectionName: sn, + } + } + + backendTLSPolicy := func(name, serviceName string, namespace, sectionName *string) *gatewayapi_v1alpha2.BackendTLSPolicy { + return &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: targetRef("", "Service", serviceName, namespace, sectionName), + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1beta1.LocalObjectReference{ + { + Group: "", + Kind: "Secret", + Name: "ca", + }, + }, + Hostname: "example.com", + }, + }, + } + } + + tests := map[string]struct { + targetRef gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName + backendTLSPolicies []*gatewayapi_v1alpha2.BackendTLSPolicy + want *gatewayapi_v1alpha2.BackendTLSPolicy + wantFound bool + }{ + "finds the BackendTLSPolicy with the matching targetRef": { + targetRef: targetRef("", "Service", "backend-service", nil, nil), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + backendTLSPolicy("btp1", "backend-service-with-section-name", nil, ptr.To("https")), + backendTLSPolicy("btp2", "backend-service-with-section-name", nil, ptr.To("https2")), + }, + want: backendTLSPolicy("btp", "backend-service", nil, nil), + wantFound: true, + }, + "finds the BackendTLSPolicy matching targetRef with section name": { + targetRef: targetRef("", "Service", "backend-service-with-section-name", nil, ptr.To("https2")), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + backendTLSPolicy("btp1", "backend-service-with-section-name", nil, ptr.To("https")), + backendTLSPolicy("btp2", "backend-service-with-section-name", nil, ptr.To("https2")), + }, + want: backendTLSPolicy("btp2", "backend-service-with-section-name", nil, ptr.To("https2")), + wantFound: true, + }, + "finds the fallback BackendTLSPolicy matching targetRef but not section name": { + targetRef: targetRef("", "Service", "backend-service-with-fallback", nil, ptr.To("https2")), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + backendTLSPolicy("btp1", "backend-service-with-fallback", nil, nil), + backendTLSPolicy("btp2", "backend-service-with-fallback", nil, ptr.To("https")), + }, + want: backendTLSPolicy("btp1", "backend-service-with-fallback", nil, nil), + wantFound: true, + }, + "finds the fallback BackendTLSPolicy matching targetRef with section name": { + targetRef: targetRef("", "Service", "backend-service-with-fallback", nil, ptr.To("https")), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + backendTLSPolicy("btp1", "backend-service-with-fallback", nil, nil), + backendTLSPolicy("btp2", "backend-service-with-fallback", nil, ptr.To("https")), + }, + want: backendTLSPolicy("btp2", "backend-service-with-fallback", nil, ptr.To("https")), + wantFound: true, + }, + "finds the BackendTLSPolicy matching namespace": { + targetRef: targetRef("", "Service", "backend-service-with-ns", ptr.To("some-ns"), nil), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + backendTLSPolicy("btp1", "backend-service-with-other-ns", ptr.To("other-ns"), nil), + backendTLSPolicy("btp2", "backend-service-with-ns", ptr.To("some-ns"), nil), + }, + want: backendTLSPolicy("btp2", "backend-service-with-ns", ptr.To("some-ns"), nil), + wantFound: true, + }, + "does not find the BackendTLSPolicy if the namespace does not match": { + targetRef: targetRef("", "Service", "backend-service", ptr.To("not-default"), nil), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + }, + wantFound: false, + }, + "does not find the BackendTLSPolicy if the service name does not match": { + targetRef: targetRef("", "Service", "other-service", nil, nil), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + }, + wantFound: false, + }, + "does not find the BackendTLSPolicy if the GroupKind does not match": { + targetRef: targetRef("example.api", "ExampleService", "backend-service", nil, nil), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + }, + wantFound: false, + }, + "does not find the BackendTLSPolicy if the group does not match": { + targetRef: targetRef("core", "Service", "backend-service", ptr.To("not-default"), nil), + backendTLSPolicies: []*gatewayapi_v1alpha2.BackendTLSPolicy{ + backendTLSPolicy("btp", "backend-service", nil, nil), + }, + wantFound: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + cache := KubernetesCache{ + FieldLogger: fixture.NewTestLogger(t), + } + + for _, backendTLSPolicy := range tc.backendTLSPolicies { + cache.Insert(backendTLSPolicy) + } + + gotBTP, gotFound := cache.LookupBackendTLSPolicyByTargetRef(tc.targetRef) + + if tc.wantFound { + assert.True(t, gotFound) + assert.Equal(t, tc.want, gotBTP) + } else { + assert.False(t, gotFound) + assert.Nil(t, gotBTP) + } + }) + } +} + +func TestLookupCAConfigMap(t *testing.T) { + cache := func(objs ...any) *KubernetesCache { + cache := KubernetesCache{ + FieldLogger: fixture.NewTestLogger(t), + } + for _, o := range objs { + cache.Insert(o) + } + return &cache + } + + configmap := func(name, namespace, data string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string]string{ + CACertificateKey: data, + }, + } + } + + secret := func(name, namespace, data string) *Secret { + return &Secret{ + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: v1.SecretTypeOpaque, + Data: map[string][]byte{ + CACertificateKey: []byte(data), + }, + }, + ValidCASecret: &SecretValidationStatus{ + Error: nil, + }, + } + } + + tests := map[string]struct { + cache *KubernetesCache + meta types.NamespacedName + wantSecret *Secret + wantErr error + }{ + "finds configmap by namespacedname and returns it as dag secret": { + cache: cache( + configmap("ca", "default", fixture.CA_CERT), + configmap("another-ca", "default", fixture.EC_CERTIFICATE), + ), + meta: types.NamespacedName{Namespace: "default", Name: "ca"}, + wantSecret: secret("ca", "default", fixture.CA_CERT), + }, + "returns an error if configmap secret is not a valid cert": { + cache: cache( + configmap("ca", "default", "invalid-ca-data"), + ), + meta: types.NamespacedName{Namespace: "default", Name: "ca"}, + wantErr: errors.New("invalid CA certificate bundle: failed to locate certificate"), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + gotSecret, gotErr := tc.cache.LookupCAConfigMap(tc.meta) + + switch { + case tc.wantErr != nil: + require.Error(t, gotErr) + assert.EqualError(t, tc.wantErr, gotErr.Error()) + default: + assert.Nil(t, gotErr) + assert.Equal(t, tc.wantSecret, gotSecret) + } + }) + } +} diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 95aaf383e42..e85b1e7808f 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -668,7 +668,7 @@ type ClientCertificateDetails struct { type PeerValidationContext struct { // CACertificate holds a reference to the Secret containing the CA to be used to // verify the upstream connection. - CACertificate *Secret + CACertificates []*Secret // SubjectNames holds optional subject names which Envoy will check against the // certificate presented by the upstream. The first entry must match the value of SubjectName SubjectNames []string @@ -691,11 +691,18 @@ type PeerValidationContext struct { // GetCACertificate returns the CA certificate from PeerValidationContext. func (pvc *PeerValidationContext) GetCACertificate() []byte { - if pvc == nil || pvc.CACertificate == nil { + if pvc == nil || len(pvc.CACertificates) == 0 { // No validation required. return nil } - return pvc.CACertificate.Object.Data[CACertificateKey] + var certs []byte + for _, cert := range pvc.CACertificates { + if cert == nil { + continue + } + certs = append(certs, cert.Object.Data[CACertificateKey]...) + } + return certs } // GetSubjectName returns the SubjectNames from PeerValidationContext. diff --git a/internal/dag/dag_test.go b/internal/dag/dag_test.go index f57a931d846..7c8edb67eaf 100644 --- a/internal/dag/dag_test.go +++ b/internal/dag/dag_test.go @@ -76,10 +76,12 @@ func TestSecureVirtualHostValid(t *testing.T) { func TestPeerValidationContext(t *testing.T) { pvc1 := PeerValidationContext{ - CACertificate: &Secret{ - Object: &v1.Secret{ - Data: map[string][]byte{ - CACertificateKey: []byte("cacert"), + CACertificates: []*Secret{ + { + Object: &v1.Secret{ + Data: map[string][]byte{ + CACertificateKey: []byte("cacert"), + }, }, }, }, @@ -87,13 +89,45 @@ func TestPeerValidationContext(t *testing.T) { } pvc2 := PeerValidationContext{} var pvc3 *PeerValidationContext + pvc4 := PeerValidationContext{ + CACertificates: []*Secret{ + { + Object: &v1.Secret{ + Data: map[string][]byte{ + CACertificateKey: []byte("-cacert-"), + }, + }, + }, + { + Object: &v1.Secret{ + Data: map[string][]byte{ + CACertificateKey: []byte("-cacert2-"), + }, + }, + }, + { + Object: &v1.Secret{ + Data: map[string][]byte{}, + }, + }, + nil, + }, + SubjectNames: []string{"subject"}, + } + pvc5 := PeerValidationContext{ + CACertificates: []*Secret{}, + } - assert.Equal(t, "subject", pvc1.GetSubjectNames()[0]) + assert.ElementsMatch(t, []string{"subject"}, pvc1.GetSubjectNames()) assert.Equal(t, []byte("cacert"), pvc1.GetCACertificate()) assert.Equal(t, []string(nil), pvc2.GetSubjectNames()) assert.Equal(t, []byte(nil), pvc2.GetCACertificate()) assert.Equal(t, []string(nil), pvc3.GetSubjectNames()) assert.Equal(t, []byte(nil), pvc3.GetCACertificate()) + assert.ElementsMatch(t, []string{"subject"}, pvc4.GetSubjectNames()) + assert.Equal(t, []byte("-cacert--cacert2-"), pvc4.GetCACertificate()) + assert.Equal(t, []string(nil), pvc5.GetSubjectNames()) + assert.Equal(t, []byte(nil), pvc5.GetCACertificate()) } func TestObserverFunc(t *testing.T) { diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index eeeb4bfb579..2b7c6a29048 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -34,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapi_v1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -78,6 +79,10 @@ type GatewayAPIProcessor struct { // GlobalCircuitBreakerDefaults defines global circuit breaker defaults. GlobalCircuitBreakerDefaults *contour_api_v1alpha1.GlobalCircuitBreakerDefaults + + // UpstreamTLS defines the TLS settings like min/max version + // and cipher suites for upstream connections. + UpstreamTLS *UpstreamTLS } // matchConditions holds match rules. @@ -1976,6 +1981,74 @@ func (p *GatewayAPIProcessor) httpClusters(routeNamespace string, backendRefs [] continue } + var upstreamValidation *PeerValidationContext + var backendRefGroup gatewayapi_v1alpha2.Group + if backendRef.Group != nil { + backendRefGroup = *backendRef.Group + } + + var backendRefKind gatewayapi_v1alpha2.Kind + if backendRef.Kind != nil { + backendRefKind = *backendRef.Kind + } + + policyTargetRef := gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Group: backendRefGroup, + Kind: backendRefKind, + Name: backendRef.Name, + Namespace: backendRef.Namespace, + }, + SectionName: ptr.To(gatewayapi_v1alpha2.SectionName(service.Weighted.ServicePort.Name)), + } + + var upstreamTLS *UpstreamTLS + // Check to see if there is any BackendTLSPolicy matching this service and service port + backendTLSPolicy, found := p.source.LookupBackendTLSPolicyByTargetRef(policyTargetRef) + if found { + var caSecrets []*Secret + for _, certRef := range backendTLSPolicy.Spec.TLS.CACertRefs { + if certRef.Group != "" { + continue + } + + switch certRef.Kind { + case "Secret": + caSecret, err := p.source.LookupCASecret(types.NamespacedName{ + Name: string(certRef.Name), + Namespace: backendTLSPolicy.Namespace, + }, backendTLSPolicy.Namespace) + if err != nil { + continue + } + caSecrets = append(caSecrets, caSecret) + case "ConfigMap": + caSecret, err := p.source.LookupCAConfigMap(types.NamespacedName{ + Name: string(certRef.Name), + Namespace: backendTLSPolicy.Namespace, + }) + if err != nil { + continue + } + caSecrets = append(caSecrets, caSecret) + default: + continue + } + } + + if len(caSecrets) == 0 { + continue + } + + upstreamValidation = &PeerValidationContext{ + CACertificates: caSecrets, + SubjectNames: []string{string(backendTLSPolicy.Spec.TLS.Hostname)}, + } + + service.Protocol = "tls" + upstreamTLS = p.UpstreamTLS + } + var clusterRequestHeaderPolicy *HeadersPolicy var clusterResponseHeaderPolicy *HeadersPolicy @@ -2037,6 +2110,8 @@ func (p *GatewayAPIProcessor) httpClusters(routeNamespace string, backendRefs [] TimeoutPolicy: ClusterTimeoutPolicy{ConnectTimeout: p.ConnectTimeout}, MaxRequestsPerConnection: p.MaxRequestsPerConnection, PerConnectionBufferLimitBytes: p.PerConnectionBufferLimitBytes, + UpstreamValidation: upstreamValidation, + UpstreamTLS: upstreamTLS, }) } return clusters, totalWeight, true diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index f6a43c57a93..d5720c5348d 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -340,7 +340,9 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) { } return } - dv.CACertificate = cacert + dv.CACertificates = []*Secret{ + cacert, + } } else if !tls.ClientValidation.SkipClientCertValidation { validCond.AddErrorf(contour_api_v1.ConditionTypeTLSError, "ClientValidationInvalid", "Spec.VirtualHost.TLS client validation is invalid: CA Secret must be specified") diff --git a/internal/envoy/cluster.go b/internal/envoy/cluster.go index 8048b371a93..60afe0d8e29 100644 --- a/internal/envoy/cluster.go +++ b/internal/envoy/cluster.go @@ -47,8 +47,12 @@ func Clustername(cluster *dag.Cluster) string { buf += hc.Path } if uv := cluster.UpstreamValidation; uv != nil { - buf += uv.CACertificate.Object.ObjectMeta.Name - buf += uv.SubjectNames[0] + if len(uv.CACertificates) > 0 { + buf += uv.CACertificates[0].Object.ObjectMeta.Name + } + if len(uv.SubjectNames) > 0 { + buf += uv.SubjectNames[0] + } } buf += cluster.Protocol + cluster.SNI if !cluster.TimeoutPolicy.IdleConnectionTimeout.UseDefault() { diff --git a/internal/envoy/v3/auth_test.go b/internal/envoy/v3/auth_test.go index 6ddd906a61e..1e178b0b827 100644 --- a/internal/envoy/v3/auth_test.go +++ b/internal/envoy/v3/auth_test.go @@ -59,7 +59,9 @@ func TestUpstreamTLSContext(t *testing.T) { }, "no alpn, missing altname": { validation: &dag.PeerValidationContext{ - CACertificate: secret, + CACertificates: []*dag.Secret{ + secret, + }, }, want: &envoy_v3_tls.UpstreamTlsContext{ CommonTlsContext: &envoy_v3_tls.CommonTlsContext{}, @@ -75,8 +77,10 @@ func TestUpstreamTLSContext(t *testing.T) { }, "no alpn, ca and altname": { validation: &dag.PeerValidationContext{ - CACertificate: secret, - SubjectNames: []string{"www.example.com"}, + CACertificates: []*dag.Secret{ + secret, + }, + SubjectNames: []string{"www.example.com"}, }, want: &envoy_v3_tls.UpstreamTlsContext{ CommonTlsContext: &envoy_v3_tls.CommonTlsContext{ @@ -125,7 +129,9 @@ func TestUpstreamTLSContext(t *testing.T) { }, "multiple subjectnames": { validation: &dag.PeerValidationContext{ - CACertificate: secret, + CACertificates: []*dag.Secret{ + secret, + }, SubjectNames: []string{ "foo.com", "bar.com", diff --git a/internal/envoy/v3/cluster_test.go b/internal/envoy/v3/cluster_test.go index bfedbc7924e..76c3192e4df 100644 --- a/internal/envoy/v3/cluster_test.go +++ b/internal/envoy/v3/cluster_test.go @@ -299,8 +299,10 @@ func TestCluster(t *testing.T) { Upstream: service(s1, "tls"), Protocol: "tls", UpstreamValidation: &dag.PeerValidationContext{ - CACertificate: secret, - SubjectNames: []string{"foo.bar.io"}, + CACertificates: []*dag.Secret{ + secret, + }, + SubjectNames: []string{"foo.bar.io"}, }, }, want: &envoy_cluster_v3.Cluster{ @@ -314,8 +316,10 @@ func TestCluster(t *testing.T) { TransportSocket: UpstreamTLSTransportSocket( UpstreamTLSContext( &dag.PeerValidationContext{ - CACertificate: secret, - SubjectNames: []string{"foo.bar.io"}, + CACertificates: []*dag.Secret{ + secret, + }, + SubjectNames: []string{"foo.bar.io"}, }, "", nil, @@ -328,8 +332,10 @@ func TestCluster(t *testing.T) { Upstream: service(s1, "tls"), Protocol: "tls", UpstreamValidation: &dag.PeerValidationContext{ - CACertificate: secret, - SubjectNames: []string{"foo.bar.io"}, + CACertificates: []*dag.Secret{ + secret, + }, + SubjectNames: []string{"foo.bar.io"}, }, UpstreamTLS: &dag.UpstreamTLS{ MinimumProtocolVersion: "1.3", @@ -347,8 +353,10 @@ func TestCluster(t *testing.T) { TransportSocket: UpstreamTLSTransportSocket( UpstreamTLSContext( &dag.PeerValidationContext{ - CACertificate: secret, - SubjectNames: []string{"foo.bar.io"}, + CACertificates: []*dag.Secret{ + secret, + }, + SubjectNames: []string{"foo.bar.io"}, }, "", nil, @@ -935,10 +943,12 @@ func TestDNSNameCluster(t *testing.T) { Port: 443, DNSLookupFamily: "auto", UpstreamValidation: &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - Data: map[string][]byte{ - "ca.crt": []byte("ca-cert"), + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + Data: map[string][]byte{ + "ca.crt": []byte("ca-cert"), + }, }, }, }, @@ -966,10 +976,12 @@ func TestDNSNameCluster(t *testing.T) { }, }, TransportSocket: UpstreamTLSTransportSocket(UpstreamTLSContext(&dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - Data: map[string][]byte{ - "ca.crt": []byte("ca-cert"), + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + Data: map[string][]byte{ + "ca.crt": []byte("ca-cert"), + }, }, }, }, @@ -1093,14 +1105,16 @@ func TestClustername(t *testing.T) { }, LoadBalancerPolicy: "Random", UpstreamValidation: &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: []byte("somethingsecret"), + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: []byte("somethingsecret"), + }, }, }, }, diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index 01a4854ac0b..0040a33c486 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -302,14 +302,16 @@ func TestDownstreamTLSContext(t *testing.T) { } peerValidationContext := &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: ca, + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: ca, + }, }, }, }, @@ -317,14 +319,16 @@ func TestDownstreamTLSContext(t *testing.T) { // Negative test case: downstream validation should not contain subjectname. peerValidationContextWithSubjectName := &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: ca, + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: ca, + }, }, }, }, @@ -340,14 +344,16 @@ func TestDownstreamTLSContext(t *testing.T) { }, } peerValidationContextSkipClientCertValidationWithCA := &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: ca, + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: ca, + }, }, }, }, @@ -364,28 +370,32 @@ func TestDownstreamTLSContext(t *testing.T) { }, } peerValidationContextOptionalClientCertValidationWithCA := &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: ca, + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: ca, + }, }, }, }, OptionalClientCertificate: true, } peerValidationContextWithCRLCheck := &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: ca, + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: ca, + }, }, }, }, @@ -417,14 +427,16 @@ func TestDownstreamTLSContext(t *testing.T) { } peerValidationContextWithCRLCheckOnlyLeaf := &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "default", - }, - Data: map[string][]byte{ - dag.CACertificateKey: ca, + CACertificates: []*dag.Secret{ + { + Object: &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: map[string][]byte{ + dag.CACertificateKey: ca, + }, }, }, }, diff --git a/internal/featuretests/v3/backendclientauth_test.go b/internal/featuretests/v3/backendclientauth_test.go index ab168a87b90..78bdfd45d1a 100644 --- a/internal/featuretests/v3/backendclientauth_test.go +++ b/internal/featuretests/v3/backendclientauth_test.go @@ -177,8 +177,8 @@ func TestBackendClientAuthenticationWithExtensionService(t *testing.T) { tlsSocket := envoy_v3.UpstreamTLSTransportSocket( envoy_v3.UpstreamTLSContext( &dag.PeerValidationContext{ - CACertificate: &dag.Secret{Object: featuretests.CASecret(t, "secret", &featuretests.CACertificate)}, - SubjectNames: []string{"subjname"}, + CACertificates: []*dag.Secret{{Object: featuretests.CASecret(t, "secret", &featuretests.CACertificate)}}, + SubjectNames: []string{"subjname"}, }, "subjname", &dag.Secret{Object: clientSecret}, diff --git a/internal/featuretests/v3/downstreamvalidation_test.go b/internal/featuretests/v3/downstreamvalidation_test.go index 1322aee9a28..3642c488b31 100644 --- a/internal/featuretests/v3/downstreamvalidation_test.go +++ b/internal/featuretests/v3/downstreamvalidation_test.go @@ -72,8 +72,10 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) { filterchaintls("example.com", serverTLSSecret, httpsFilterFor("example.com"), &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: clientCASecret, + CACertificates: []*dag.Secret{ + { + Object: clientCASecret, + }, }, }, "h2", "http/1.1", @@ -171,8 +173,10 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) { httpsFilterFor("example.com"), &dag.PeerValidationContext{ SkipClientCertValidation: true, - CACertificate: &dag.Secret{ - Object: clientCASecret, + CACertificates: []*dag.Secret{ + { + Object: clientCASecret, + }, }, }, "h2", "http/1.1", @@ -224,8 +228,10 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) { filterchaintls("example.com", serverTLSSecret, httpsFilterFor("example.com"), &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: clientCASecret, + CACertificates: []*dag.Secret{ + { + Object: clientCASecret, + }, }, CRL: &dag.Secret{ Object: crlSecret, @@ -276,8 +282,10 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) { filterchaintls("example.com", serverTLSSecret, httpsFilterFor("example.com"), &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: clientCASecret, + CACertificates: []*dag.Secret{ + { + Object: clientCASecret, + }, }, CRL: &dag.Secret{ Object: crlSecret, @@ -328,8 +336,10 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) { filterchaintls("example.com", serverTLSSecret, httpsFilterFor("example.com"), &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: clientCASecret, + CACertificates: []*dag.Secret{ + { + Object: clientCASecret, + }, }, OptionalClientCertificate: true, }, @@ -389,8 +399,10 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) { URI: true, }), &dag.PeerValidationContext{ - CACertificate: &dag.Secret{ - Object: clientCASecret, + CACertificates: []*dag.Secret{ + { + Object: clientCASecret, + }, }, }, "h2", "http/1.1", diff --git a/internal/featuretests/v3/envoy.go b/internal/featuretests/v3/envoy.go index 1f97413aa69..4ebafe2a96a 100644 --- a/internal/featuretests/v3/envoy.go +++ b/internal/featuretests/v3/envoy.go @@ -190,16 +190,16 @@ func tlsCluster(c *envoy_cluster_v3.Cluster, ca *v1.Secret, subjectName, sni str } // Secret for validation is optional. - var s *dag.Secret + var s []*dag.Secret if ca != nil { - s = &dag.Secret{Object: ca} + s = []*dag.Secret{{Object: ca}} } c.TransportSocket = envoy_v3.UpstreamTLSTransportSocket( envoy_v3.UpstreamTLSContext( &dag.PeerValidationContext{ - CACertificate: s, - SubjectNames: []string{subjectName}, + CACertificates: s, + SubjectNames: []string{subjectName}, }, sni, secret, diff --git a/internal/featuretests/v3/upstreamtls_test.go b/internal/featuretests/v3/upstreamtls_test.go index 52367a24c5f..4fbce528765 100644 --- a/internal/featuretests/v3/upstreamtls_test.go +++ b/internal/featuretests/v3/upstreamtls_test.go @@ -28,12 +28,16 @@ import ( envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" "github.com/projectcontour/contour/internal/featuretests" "github.com/projectcontour/contour/internal/fixture" + "github.com/projectcontour/contour/internal/gatewayapi" "github.com/projectcontour/contour/internal/ref" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" networking_v1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + gatewayapi_v1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) func TestUpstreamTLSWithHTTPProxy(t *testing.T) { @@ -223,3 +227,119 @@ func TestUpstreamTLSWithExtensionService(t *testing.T) { ), }) } + +func TestUpstreamTLSWithHTTPRoute(t *testing.T) { + rh, c, done := setup(t, func(b *dag.Builder) { + for _, processor := range b.Processors { + if gatewayAPIProcessor, ok := processor.(*dag.GatewayAPIProcessor); ok { + gatewayAPIProcessor.UpstreamTLS = &dag.UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + } + } + } + }) + defer done() + + sec1 := featuretests.TLSSecret(t, "sec1", &featuretests.ClientCertificate) + sec2 := featuretests.CASecret(t, "sec2", &featuretests.CACertificate) + rh.OnAdd(sec1) + rh.OnAdd(sec2) + + rh.OnAdd(&gatewayapi_v1beta1.GatewayClass{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: fixture.ObjectMeta("test-gc"), + Spec: gatewayapi_v1beta1.GatewayClassSpec{ + ControllerName: "projectcontour.io/contour", + }, + Status: gatewayapi_v1beta1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: string(gatewayapi_v1.GatewayClassConditionStatusAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + + gateway := &gatewayapi_v1beta1.Gateway{ + ObjectMeta: fixture.ObjectMeta("projectcontour/contour"), + Spec: gatewayapi_v1beta1.GatewaySpec{ + Listeners: []gatewayapi_v1beta1.Listener{{ + Name: "http", + Port: 80, + Protocol: gatewayapi_v1.HTTPProtocolType, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1.NamespacesFromAll), + }, + }, + }}, + }, + } + rh.OnAdd(gateway) + + svc := fixture.NewService("backend"). + WithPorts(v1.ServicePort{Name: "http", Port: 443}) + rh.OnAdd(svc) + + rh.OnAdd(&gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "authenticated", + Namespace: "default", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{ + gatewayapi.GatewayParentRef("projectcontour", "contour"), + }, + }, + Hostnames: []gatewayapi_v1beta1.Hostname{ + "test.projectcontour.io", + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{{ + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/"), + BackendRefs: gatewayapi.HTTPBackendRef("backend", 443, 1), + }}, + }, + }) + + rh.OnAdd(&gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "authenticated", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "backend", + }, + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1alpha2.LocalObjectReference{{ + Kind: "Secret", + Name: gatewayapi_v1.ObjectName(sec2.Name), + }}, + Hostname: "subjname", + }, + }, + }) + + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + tlsCluster( + cluster("default/backend/443/950c17581f", "default/backend/http", "default_backend_443"), + sec2, + "subjname", + "", + nil, + &dag.UpstreamTLS{ + MinimumProtocolVersion: "1.2", + MaximumProtocolVersion: "1.2", + }), + ), + TypeUrl: clusterType, + }) + +} diff --git a/internal/k8s/rbac.go b/internal/k8s/rbac.go index 2eccadecc89..3bd43c64d3d 100644 --- a/internal/k8s/rbac.go +++ b/internal/k8s/rbac.go @@ -19,10 +19,10 @@ package k8s // +kubebuilder:rbac:groups="projectcontour.io",resources=httpproxies;tlscertificatedelegations;extensionservices;contourconfigurations,verbs=get;list;watch // +kubebuilder:rbac:groups="projectcontour.io",resources=httpproxies/status;extensionservices/status;contourconfigurations/status,verbs=create;get;update -// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;tlsroutes;grpcroutes;tcproutes;referencegrants,verbs=get;list;watch -// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;tlsroutes/status;grpcroutes/status;tcproutes/status,verbs=update +// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;tlsroutes;grpcroutes;tcproutes;referencegrants;backendtlspolicies,verbs=get;list;watch +// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;tlsroutes/status;grpcroutes/status;tcproutes/status;backendtlspolicies/status,verbs=update -// +kubebuilder:rbac:groups="",resources=secrets;endpoints;services;namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=secrets;endpoints;services;namespaces;configmaps,verbs=get;list;watch // Add RBAC policy to support leader election. // +kubebuilder:rbac:groups="",resources=events,verbs=create;get;update,namespace=projectcontour diff --git a/internal/provisioner/objects/rbac/clusterrole/cluster_role.go b/internal/provisioner/objects/rbac/clusterrole/cluster_role.go index b610bc7e687..2ab5b1b2869 100644 --- a/internal/provisioner/objects/rbac/clusterrole/cluster_role.go +++ b/internal/provisioner/objects/rbac/clusterrole/cluster_role.go @@ -75,15 +75,15 @@ func desiredClusterRole(name string, contour *model.Contour) *rbacv1.ClusterRole }, Rules: []rbacv1.PolicyRule{ // Core Contour-watched resources. - policyRuleFor(corev1.GroupName, getListWatch, "secrets", "endpoints", "services", "namespaces"), + policyRuleFor(corev1.GroupName, getListWatch, "secrets", "endpoints", "services", "namespaces", "configmaps"), // Discovery Contour-watched resources. policyRuleFor(discoveryv1.GroupName, getListWatch, "endpointslices"), // Gateway API resources. // Note, ReferenceGrant does not currently have a .status field so it's omitted from the status rule. - policyRuleFor(gatewayv1alpha2.GroupName, getListWatch, "gatewayclasses", "gateways", "httproutes", "tlsroutes", "grpcroutes", "tcproutes", "referencegrants"), - policyRuleFor(gatewayv1alpha2.GroupName, update, "gatewayclasses/status", "gateways/status", "httproutes/status", "tlsroutes/status", "grpcroutes/status", "tcproutes/status"), + policyRuleFor(gatewayv1alpha2.GroupName, getListWatch, "gatewayclasses", "gateways", "httproutes", "tlsroutes", "grpcroutes", "tcproutes", "referencegrants", "backendtlspolicies"), + policyRuleFor(gatewayv1alpha2.GroupName, update, "gatewayclasses/status", "gateways/status", "httproutes/status", "tlsroutes/status", "grpcroutes/status", "tcproutes/status", "backendtlspolicies/status"), // Ingress resources. policyRuleFor(networkingv1.GroupName, getListWatch, "ingresses"), diff --git a/test/e2e/fixtures.go b/test/e2e/fixtures.go index 675d0f76a07..bbab25500fd 100644 --- a/test/e2e/fixtures.go +++ b/test/e2e/fixtures.go @@ -287,7 +287,7 @@ type EchoSecure struct { // fails the test if it encounters an error. Namespace is defaulted to "default" // and name is defaulted to "ingress-conformance-echo-tls" if not provided. Returns // a cleanup function. -func (e *EchoSecure) Deploy(ns, name string) func() { +func (e *EchoSecure) Deploy(ns, name string, preApplyHook func(deployment *appsv1.Deployment, service *corev1.Service)) func() { ns = valOrDefault(ns, "default") name = valOrDefault(name, "ingress-conformance-echo-tls") @@ -388,7 +388,6 @@ func (e *EchoSecure) Deploy(ns, name string) func() { }, }, } - require.NoError(e.t, e.client.Create(context.TODO(), deployment)) service := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -414,6 +413,12 @@ func (e *EchoSecure) Deploy(ns, name string) func() { Selector: map[string]string{"app.kubernetes.io/name": name}, }, } + + if preApplyHook != nil { + preApplyHook(deployment, service) + } + + require.NoError(e.t, e.client.Create(context.TODO(), deployment)) require.NoError(e.t, e.client.Create(context.TODO(), service)) return func() { diff --git a/test/e2e/framework.go b/test/e2e/framework.go index 7ab06cb85b3..0d7e41866d5 100644 --- a/test/e2e/framework.go +++ b/test/e2e/framework.go @@ -389,6 +389,12 @@ func (f *Framework) CreateTCPRouteAndWaitFor(route *gatewayapi_v1alpha2.TCPRoute return createAndWaitFor(f.t, f.Client, route, condition, f.RetryInterval, f.RetryTimeout) } +// CreateBackendTLSPolicy creates the provided BackendTLSPolicy in the Kubernetes API +// and then waits for the specified condition to be true. +func (f *Framework) CreateBackendTLSPolicyAndWaitFor(route *gatewayapi_v1alpha2.BackendTLSPolicy, condition func(*gatewayapi_v1alpha2.BackendTLSPolicy) bool) (*gatewayapi_v1alpha2.BackendTLSPolicy, bool) { + return createAndWaitFor(f.t, f.Client, route, condition, f.RetryInterval, f.RetryTimeout) +} + // CreateNamespace creates a namespace with the given name in the // Kubernetes API or fails the test if it encounters an error. func (f *Framework) CreateNamespace(name string) { diff --git a/test/e2e/gateway/backend_tls_policy_test.go b/test/e2e/gateway/backend_tls_policy_test.go new file mode 100644 index 00000000000..ca65c09d570 --- /dev/null +++ b/test/e2e/gateway/backend_tls_policy_test.go @@ -0,0 +1,176 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package gateway + +import ( + "context" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + . "github.com/onsi/ginkgo/v2" + "github.com/projectcontour/contour/internal/gatewayapi" + "github.com/projectcontour/contour/test/e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gatewayapi_v1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func testBackendTLSPolicy(namespace string, gateway types.NamespacedName) { + Specify("Creates a BackendTLSPolicy configures an HTTPRoute to use TLS to a backend service", func() { + t := f.T() + + // Top level issuer. + selfSignedIssuer := &certmanagerv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "selfsigned", + }, + Spec: certmanagerv1.IssuerSpec{ + IssuerConfig: certmanagerv1.IssuerConfig{ + SelfSigned: &certmanagerv1.SelfSignedIssuer{}, + }, + }, + } + require.NoError(f.T(), f.Client.Create(context.TODO(), selfSignedIssuer)) + + // CA to sign backend certs with. + caCertificate := &certmanagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ca-cert", + }, + Spec: certmanagerv1.CertificateSpec{ + IsCA: true, + Usages: []certmanagerv1.KeyUsage{ + certmanagerv1.UsageSigning, + certmanagerv1.UsageCertSign, + }, + CommonName: "ca-cert", + SecretName: "ca-cert", + IssuerRef: certmanagermetav1.ObjectReference{ + Name: "selfsigned", + }, + }, + } + require.NoError(f.T(), f.Client.Create(context.TODO(), caCertificate)) + + // Issuer based on CA to generate new certs with. + basedOnCAIssuer := &certmanagerv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "ca-issuer", + }, + Spec: certmanagerv1.IssuerSpec{ + IssuerConfig: certmanagerv1.IssuerConfig{ + CA: &certmanagerv1.CAIssuer{ + SecretName: "ca-cert", + }, + }, + }, + } + require.NoError(f.T(), f.Client.Create(context.TODO(), basedOnCAIssuer)) + + // Backend server cert signed by CA. + backendServerCert := &certmanagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "backend-server-cert", + }, + Spec: certmanagerv1.CertificateSpec{ + Usages: []certmanagerv1.KeyUsage{ + certmanagerv1.UsageServerAuth, + }, + CommonName: "echo-secure", + DNSNames: []string{"echo-secure"}, + SecretName: "backend-server-cert", + IssuerRef: certmanagermetav1.ObjectReference{ + Name: "ca-issuer", + }, + }, + } + + require.NoError(f.T(), f.Client.Create(context.TODO(), backendServerCert)) + f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure", func(deployment *appsv1.Deployment, service *corev1.Service) { + delete(service.Annotations, "projectcontour.io/upstream-protocol.tls") + }) + + route := &gatewayapi_v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "http-route-1", + }, + Spec: gatewayapi_v1beta1.HTTPRouteSpec{ + Hostnames: []gatewayapi_v1beta1.Hostname{"backend-tls-policy.projectcontour.io"}, + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{ + gatewayapi.GatewayParentRef(gateway.Namespace, gateway.Name), + }, + }, + Rules: []gatewayapi_v1beta1.HTTPRouteRule{ + { + + Matches: gatewayapi.HTTPRouteMatch(gatewayapi_v1.PathMatchPathPrefix, "/"), + BackendRefs: gatewayapi.HTTPBackendRef("echo-secure", 443, 1), + }, + }, + }, + } + f.CreateHTTPRouteAndWaitFor(route, e2e.HTTPRouteAccepted) + + backendTLSPolicy := &gatewayapi_v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "echo-secure-backend-tls-policy", + Namespace: namespace, + }, + Spec: gatewayapi_v1alpha2.BackendTLSPolicySpec{ + TargetRef: gatewayapi_v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gatewayapi_v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "echo-secure", + }, + }, + TLS: gatewayapi_v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayapi_v1.LocalObjectReference{ + { + Group: "", + Kind: "Secret", + Name: "backend-server-cert", + }, + }, + Hostname: "echo-secure", + }, + }, + } + + f.CreateBackendTLSPolicyAndWaitFor(backendTLSPolicy, e2e.BackendTLSPolicyAccepted) + + // Ensure http (insecure) request routes to echo-secure. + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: "backend-tls-policy.projectcontour.io", + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res) + assert.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + assert.Equal(t, "echo-secure", f.GetEchoResponseBody(res.Body).Service) + }) +} diff --git a/test/e2e/gateway/gateway_test.go b/test/e2e/gateway/gateway_test.go index b00470af2c4..deda76170e5 100644 --- a/test/e2e/gateway/gateway_test.go +++ b/test/e2e/gateway/gateway_test.go @@ -207,6 +207,8 @@ var _ = Describe("Gateway API", func() { f.NamespacedTest("gateway-host-rewrite", testWithHTTPGateway(testHostRewrite)) f.NamespacedTest("gateway-request-redirect-rule", testWithHTTPGateway(testRequestRedirectRule)) + + f.NamespacedTest("gateway-backend-tls-policy", testWithHTTPGateway(testBackendTLSPolicy)) }) Describe("Gateway with one HTTP listener and one HTTPS listener", func() { diff --git a/test/e2e/gatewayapi_predicates.go b/test/e2e/gatewayapi_predicates.go index 509ad91c41f..3200cd74768 100644 --- a/test/e2e/gatewayapi_predicates.go +++ b/test/e2e/gatewayapi_predicates.go @@ -136,6 +136,18 @@ func TCPRouteAccepted(route *gatewayapi_v1alpha2.TCPRoute) bool { return false } +// BackendTLSPolicyAccepted returns true if the backend TLS policy has a .status.conditions +// entry of "Accepted: true". +func BackendTLSPolicyAccepted(btp *gatewayapi_v1alpha2.BackendTLSPolicy) bool { + if btp == nil { + return false + } + + // TODO (christianang): Right now this always returns true since status conditions are not implemented yet + // for BackendTLSPolicy + return true +} + func conditionExists(conditions []metav1.Condition, conditionType string, conditionStatus metav1.ConditionStatus) bool { for _, cond := range conditions { if cond.Type == conditionType && cond.Status == conditionStatus { diff --git a/test/e2e/httpproxy/backend_tls_protocol_version_test.go b/test/e2e/httpproxy/backend_tls_protocol_version_test.go index d3ae8a39221..9d47fb227d4 100644 --- a/test/e2e/httpproxy/backend_tls_protocol_version_test.go +++ b/test/e2e/httpproxy/backend_tls_protocol_version_test.go @@ -50,7 +50,7 @@ func testBackendTLSProtocolVersion(namespace, protocolVersion string) { }, } require.NoError(f.T(), f.Client.Create(context.TODO(), backendServerCert)) - f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure") + f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure", nil) p := &contourv1.HTTPProxy{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e/httpproxy/backend_tls_test.go b/test/e2e/httpproxy/backend_tls_test.go index d1c2be67860..d69dd450624 100644 --- a/test/e2e/httpproxy/backend_tls_test.go +++ b/test/e2e/httpproxy/backend_tls_test.go @@ -53,7 +53,7 @@ func testBackendTLS(namespace string) { }, } require.NoError(f.T(), f.Client.Create(context.TODO(), backendServerCert)) - f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure") + f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure", nil) p := &contourv1.HTTPProxy{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e/httpproxy/external_name_test.go b/test/e2e/httpproxy/external_name_test.go index 15ebcd3ff39..022a2a84853 100644 --- a/test/e2e/httpproxy/external_name_test.go +++ b/test/e2e/httpproxy/external_name_test.go @@ -101,7 +101,7 @@ func testExternalNameServiceTLS(namespace string) { f.Certs.CreateSelfSignedCert(namespace, "backend-server-cert", "backend-server-cert", "echo") - f.Fixtures.EchoSecure.Deploy(namespace, "echo-tls") + f.Fixtures.EchoSecure.Deploy(namespace, "echo-tls", nil) externalNameService := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e/ingress/backend_tls_test.go b/test/e2e/ingress/backend_tls_test.go index 4d100f8483e..0a862a58588 100644 --- a/test/e2e/ingress/backend_tls_test.go +++ b/test/e2e/ingress/backend_tls_test.go @@ -53,7 +53,7 @@ func testBackendTLS(namespace string) { }, } require.NoError(f.T(), f.Client.Create(context.TODO(), backendServerCert)) - f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure") + f.Fixtures.EchoSecure.Deploy(namespace, "echo-secure", nil) i := &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{