From f1ede3e7bde27e138ac20ae9d5df3a78010d8ab7 Mon Sep 17 00:00:00 2001 From: huabing zhao Date: Thu, 12 Oct 2023 15:45:55 +0800 Subject: [PATCH] init SecurityPolicy Signed-off-by: huabing zhao --- api/v1alpha1/securitypolicy_types.go | 68 ++++ api/v1alpha1/zz_generated.deepcopy.go | 97 +++++ internal/gatewayapi/resource.go | 2 + internal/gatewayapi/runner/runner.go | 6 +- internal/gatewayapi/securitypolicy.go | 231 +++++++++++ .../securitypolicy-status-conditions.in.yaml | 133 ++++++ .../securitypolicy-status-conditions.out.yaml | 384 ++++++++++++++++++ internal/gatewayapi/translator.go | 13 +- internal/gatewayapi/zz_generated.deepcopy.go | 11 + internal/message/types.go | 2 + internal/provider/kubernetes/controller.go | 13 + internal/status/securitypolicy.go | 20 + site/content/en/latest/api/extension_types.md | 51 +++ 13 files changed, 1028 insertions(+), 3 deletions(-) create mode 100644 api/v1alpha1/securitypolicy_types.go create mode 100644 internal/gatewayapi/securitypolicy.go create mode 100644 internal/gatewayapi/testdata/securitypolicy-status-conditions.in.yaml create mode 100755 internal/gatewayapi/testdata/securitypolicy-status-conditions.out.yaml create mode 100644 internal/status/securitypolicy.go diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go new file mode 100644 index 000000000000..f53d237323ad --- /dev/null +++ b/api/v1alpha1/securitypolicy_types.go @@ -0,0 +1,68 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +const ( + // KindSecurityPolicy is the name of the SecurityPolicy kind. + KindSecurityPolicy = "SecurityPolicy" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Accepted")].reason` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// SecurityPolicy allows the user to configure various security settings for a +// Gateway. +type SecurityPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of SecurityPolicy. + Spec SecurityPolicySpec `json:"spec"` + + // Status defines the current status of SecurityPolicy. + Status SecurityPolicyStatus `json:"status,omitempty"` +} + +// SecurityPolicySpec defines the desired state of SecurityPolicy. +type SecurityPolicySpec struct { + // TargetRef is the name of the Gateway resource this policy + // is being attached to. + // This Policy and the TargetRef MUST be in the same namespace + // for this Policy to have effect and be applied to the Gateway. + // TargetRef + TargetRef gwapiv1a2.PolicyTargetReferenceWithSectionName `json:"targetRef"` +} + +// SecurityPolicyStatus defines the state of SecurityPolicy +type SecurityPolicyStatus struct { + // Conditions describe the current conditions of the SecurityPolicy. + // + // +optional + // +listType=map + // +listMapKey=type + // +kubebuilder:validation:MaxItems=8 + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true + +// SecurityPolicyList contains a list of SecurityPolicy resources. +type SecurityPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SecurityPolicy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SecurityPolicy{}, &SecurityPolicyList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c9b8600c45f7..917d79ce6809 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1966,6 +1966,103 @@ func (in *RequestHeaderCustomTag) DeepCopy() *RequestHeaderCustomTag { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityPolicy) DeepCopyInto(out *SecurityPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicy. +func (in *SecurityPolicy) DeepCopy() *SecurityPolicy { + if in == nil { + return nil + } + out := new(SecurityPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SecurityPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityPolicyList) DeepCopyInto(out *SecurityPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SecurityPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicyList. +func (in *SecurityPolicyList) DeepCopy() *SecurityPolicyList { + if in == nil { + return nil + } + out := new(SecurityPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SecurityPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityPolicySpec) DeepCopyInto(out *SecurityPolicySpec) { + *out = *in + in.TargetRef.DeepCopyInto(&out.TargetRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicySpec. +func (in *SecurityPolicySpec) DeepCopy() *SecurityPolicySpec { + if in == nil { + return nil + } + out := new(SecurityPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityPolicyStatus) DeepCopyInto(out *SecurityPolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicyStatus. +func (in *SecurityPolicyStatus) DeepCopy() *SecurityPolicyStatus { + if in == nil { + return nil + } + out := new(SecurityPolicyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SourceMatch) DeepCopyInto(out *SourceMatch) { *out = *in diff --git a/internal/gatewayapi/resource.go b/internal/gatewayapi/resource.go index 6b0e87303d7a..318728afce7e 100644 --- a/internal/gatewayapi/resource.go +++ b/internal/gatewayapi/resource.go @@ -47,6 +47,7 @@ type Resources struct { EnvoyPatchPolicies []*egv1a1.EnvoyPatchPolicy `json:"envoyPatchPolicies,omitempty" yaml:"envoyPatchPolicies,omitempty"` ClientTrafficPolicies []*egv1a1.ClientTrafficPolicy `json:"clientTrafficPolicies,omitempty" yaml:"clientTrafficPolicies,omitempty"` BackendTrafficPolicies []*egv1a1.BackendTrafficPolicy `json:"backendTrafficPolicies,omitempty" yaml:"backendTrafficPolicies,omitempty"` + SecurityPolicies []*egv1a1.SecurityPolicy `json:"securityPolicies,omitempty" yaml:"securityPolicies,omitempty"` } func NewResources() *Resources { @@ -66,6 +67,7 @@ func NewResources() *Resources { EnvoyPatchPolicies: []*egv1a1.EnvoyPatchPolicy{}, ClientTrafficPolicies: []*egv1a1.ClientTrafficPolicy{}, BackendTrafficPolicies: []*egv1a1.BackendTrafficPolicy{}, + SecurityPolicies: []*egv1a1.SecurityPolicy{}, } } diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go index 9a89ba48eb4c..941286b2f58f 100644 --- a/internal/gatewayapi/runner/runner.go +++ b/internal/gatewayapi/runner/runner.go @@ -157,7 +157,11 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { key := utils.NamespacedName(backendTrafficPolicy) r.ProviderResources.BackendTrafficPolicyStatuses.Store(key, &backendTrafficPolicy.Status) } - + for _, securityPolicy := range result.SecurityPolicies { + securityPolicy := securityPolicy + key := utils.NamespacedName(securityPolicy) + r.ProviderResources.SecurityPolicyStatuses.Store(key, &securityPolicy.Status) + } }, ) r.Logger.Info("shutting down") diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go new file mode 100644 index 000000000000..24ae703e6b3d --- /dev/null +++ b/internal/gatewayapi/securitypolicy.go @@ -0,0 +1,231 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package gatewayapi + +import ( + "fmt" + "sort" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/status" + "github.com/envoyproxy/gateway/internal/utils/ptr" +) + +func ProcessSecurityPolicies(securityPolicies []*egv1a1.SecurityPolicy, + gateways []*GatewayContext, + routes []RouteContext, + xdsIR XdsIRMap) []*egv1a1.SecurityPolicy { + var res []*egv1a1.SecurityPolicy + + // Sort based on timestamp + sort.Slice(securityPolicies, func(i, j int) bool { + return securityPolicies[i].CreationTimestamp.Before(&(securityPolicies[j].CreationTimestamp)) + }) + + // First build a map out of the routes and gateways for faster lookup since users might have thousands of routes or more. + // For gateways this probably isn't quite as necessary. + routeMap := map[policyTargetRouteKey]*policyRouteTargetContext{} + for _, route := range routes { + key := policyTargetRouteKey{ + Kind: string(GetRouteType(route)), + Name: route.GetName(), + Namespace: route.GetNamespace(), + } + routeMap[key] = &policyRouteTargetContext{RouteContext: route} + } + gatewayMap := map[types.NamespacedName]*policyGatewayTargetContext{} + for _, gw := range gateways { + key := types.NamespacedName{ + Name: gw.GetName(), + Namespace: gw.GetNamespace(), + } + gatewayMap[key] = &policyGatewayTargetContext{GatewayContext: gw} + } + + // Translate + // 1. First translate Policies targeting xRoutes + // 2.. Finally, the policies targeting Gateways + + // Process the policies targeting xRoutes + for _, policy := range securityPolicies { + if policy.Spec.TargetRef.Kind != KindGateway { + policy := policy.DeepCopy() + res = append(res, policy) + + // Negative statuses have already been assigned so its safe to skip + route := resolveSecurityPolicyRouteTargetRef(policy, routeMap) + if route == nil { + continue + } + + translateSecurityPolicy(policy, xdsIR) + + // Set Accepted=True + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionTrue, + gwv1a2.PolicyReasonAccepted, + "SecurityPolicy has been accepted.", + ) + } + } + + // Process the policies targeting Gateways with a section name + for _, policy := range securityPolicies { + if policy.Spec.TargetRef.Kind == KindGateway { + policy := policy.DeepCopy() + res = append(res, policy) + + // Negative statuses have already been assigned so its safe to skip + gatewayKey := resolveSecurityPolicyGatewayTargetRef(policy, gatewayMap) + if gatewayKey == nil { + continue + } + + translateSecurityPolicy(policy, xdsIR) + + // Set Accepted=True + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionTrue, + gwv1a2.PolicyReasonAccepted, + "SecurityPolicy has been accepted.", + ) + } + } + + return res +} + +func resolveSecurityPolicyGatewayTargetRef(policy *egv1a1.SecurityPolicy, gateways map[types.NamespacedName]*policyGatewayTargetContext) *GatewayContext { + targetNs := policy.Spec.TargetRef.Namespace + // If empty, default to namespace of policy + if targetNs == nil { + targetNs = ptr.To(gwv1b1.Namespace(policy.Namespace)) + } + + // Ensure Policy and target are in the same namespace + if policy.Namespace != string(*targetNs) { + + message := fmt.Sprintf("Namespace:%s TargetRef.Namespace:%s, SecurityPolicy can only target a resource in the same namespace.", + policy.Namespace, *targetNs) + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + + // Find the Gateway + key := types.NamespacedName{ + Name: string(policy.Spec.TargetRef.Name), + Namespace: string(*targetNs), + } + gateway, ok := gateways[key] + + // Gateway not found + if !ok { + message := fmt.Sprintf("Gateway:%s not found.", policy.Spec.TargetRef.Name) + + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonTargetNotFound, + message, + ) + return nil + } + + // Check if another policy targeting the same Gateway exists + if gateway.attached { + message := "Unable to target Gateway, another SecurityPolicy has already attached to it" + + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonConflicted, + message, + ) + return nil + } + + // Set context and save + gateway.attached = true + gateways[key] = gateway + + return gateway.GatewayContext +} + +func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes map[policyTargetRouteKey]*policyRouteTargetContext) RouteContext { + targetNs := policy.Spec.TargetRef.Namespace + // If empty, default to namespace of policy + if targetNs == nil { + targetNs = ptr.To(gwv1b1.Namespace(policy.Namespace)) + } + + // Ensure Policy and target are in the same namespace + if policy.Namespace != string(*targetNs) { + + message := fmt.Sprintf("Namespace:%s TargetRef.Namespace:%s, SecurityPolicy can only target a resource in the same namespace.", + policy.Namespace, *targetNs) + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonInvalid, + message, + ) + return nil + } + + // Check if the route exists + key := policyTargetRouteKey{ + Kind: string(policy.Spec.TargetRef.Kind), + Name: string(policy.Spec.TargetRef.Name), + Namespace: string(*targetNs), + } + route, ok := routes[key] + + // Route not found + if !ok { + message := fmt.Sprintf("%s/%s/%s not found.", policy.Spec.TargetRef.Kind, string(*targetNs), policy.Spec.TargetRef.Name) + + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonTargetNotFound, + message, + ) + return nil + } + + // Check if another policy targeting the same xRoute exists + if route.attached { + message := fmt.Sprintf("Unable to target %s, another SecurityPolicy has already attached to it", string(policy.Spec.TargetRef.Kind)) + + status.SetSecurityPolicyCondition(policy, + gwv1a2.PolicyConditionAccepted, + metav1.ConditionFalse, + gwv1a2.PolicyReasonConflicted, + message, + ) + return nil + } + + // Set context and save + route.attached = true + routes[key] = route + + return route.RouteContext +} +func translateSecurityPolicy(policy *egv1a1.SecurityPolicy, xdsIR XdsIRMap) { +} diff --git a/internal/gatewayapi/testdata/securitypolicy-status-conditions.in.yaml b/internal/gatewayapi/testdata/securitypolicy-status-conditions.in.yaml new file mode 100644 index 000000000000..bdc74d838b14 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-status-conditions.in.yaml @@ -0,0 +1,133 @@ +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: target-gateway-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: target-gateway-1-as-well + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: target-httproute-in-gateway-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: envoy-gateway +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: also-target-httproute-in-gateway-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: envoy-gateway +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: target-grpcroute-in-gateway-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: grpcroute-1 + namespace: envoy-gateway +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: envoy-gateway + name: grpcroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + rules: + - matches: + - headers: + - type: Exact + name: magic + value: foo + backendRefs: + - name: service-1 + port: 8080 +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + - name: https + protocol: HTTPS + port: 443 + allowedRoutes: + namespaces: + from: Same + - name: tcp + protocol: TCP + port: 53 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/securitypolicy-status-conditions.out.yaml b/internal/gatewayapi/testdata/securitypolicy-status-conditions.out.yaml new file mode 100755 index 000000000000..d4c17232a746 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-status-conditions.out.yaml @@ -0,0 +1,384 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + name: https + port: 443 + protocol: HTTPS + - allowedRoutes: + namespaces: + from: Same + name: tcp + port: 53 + protocol: TCP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Listener must have TLS set when protocol is HTTPS. + reason: Invalid + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: https + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: tcp + supportedKinds: + - group: gateway.networking.k8s.io + kind: TCPRoute +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: envoy-gateway + spec: + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - headers: + - name: magic + type: Exact + value: foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Service envoy-gateway/service-1 not found + reason: BackendNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: envoy-gateway + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Service envoy-gateway/service-1 not found + reason: BackendNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 + envoy-gateway/gateway-2: + proxy: + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + - containerPort: 10053 + name: tcp + protocol: TCP + servicePort: 53 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-2 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: target-httproute-in-gateway-1 + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: also-target-httproute-in-gateway-1 + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Unable to target HTTPRoute, another SecurityPolicy has already attached + to it + reason: Conflicted + status: "False" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: target-grpcroute-in-gateway-2 + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: grpcroute-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: target-gateway-1 + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: target-gateway-1-as-well + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: Unable to target Gateway, another SecurityPolicy has already attached + to it + reason: Conflicted + status: "False" + type: Accepted +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 1 + valid: 0 + directResponse: + statusCode: 500 + hostname: '*' + name: httproute/envoy-gateway/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: / + envoy-gateway/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-2/http + port: 10080 + routes: + - backendWeights: + invalid: 1 + valid: 0 + directResponse: + statusCode: 500 + headerMatches: + - distinct: false + exact: foo + name: magic + hostname: '*' + name: grpcroute/envoy-gateway/grpcroute-1/rule/0/match/0/* diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index ae8279a8d4c7..b66585f235e7 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -99,6 +99,7 @@ func newTranslateResult(gateways []*GatewayContext, udpRoutes []*UDPRouteContext, clientTrafficPolicies []*egv1a1.ClientTrafficPolicy, backendTrafficPolicies []*egv1a1.BackendTrafficPolicy, + securityPolicies []*egv1a1.SecurityPolicy, xdsIR XdsIRMap, infraIR InfraIRMap) *TranslateResult { translateResult := &TranslateResult{ XdsIR: xdsIR, @@ -126,6 +127,7 @@ func newTranslateResult(gateways []*GatewayContext, translateResult.ClientTrafficPolicies = append(translateResult.ClientTrafficPolicies, clientTrafficPolicies...) translateResult.BackendTrafficPolicies = append(translateResult.BackendTrafficPolicies, backendTrafficPolicies...) + translateResult.SecurityPolicies = append(translateResult.SecurityPolicies, securityPolicies...) return translateResult } @@ -181,12 +183,19 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { for _, u := range udpRoutes { routes = append(routes, u) } - backendTrafficPolicies := ProcessBackendTrafficPolicies(resources.BackendTrafficPolicies, gateways, routes, xdsIR) + backendTrafficPolicies := ProcessBackendTrafficPolicies( + resources.BackendTrafficPolicies, gateways, routes, xdsIR) + // Process SecurityPolicies + securityPolicies := ProcessSecurityPolicies( + resources.SecurityPolicies, gateways, routes, xdsIR) // Sort xdsIR based on the Gateway API spec sortXdsIRMap(xdsIR) - return newTranslateResult(gateways, httpRoutes, grpcRoutes, tlsRoutes, tcpRoutes, udpRoutes, clientTrafficPolicies, backendTrafficPolicies, xdsIR, infraIR) + return newTranslateResult(gateways, httpRoutes, grpcRoutes, tlsRoutes, + tcpRoutes, udpRoutes, clientTrafficPolicies, backendTrafficPolicies, + securityPolicies, xdsIR, infraIR) + } // GetRelevantGateways returns GatewayContexts, containing a copy of the original diff --git a/internal/gatewayapi/zz_generated.deepcopy.go b/internal/gatewayapi/zz_generated.deepcopy.go index 6e5d0fa05298..fb847507b0a6 100644 --- a/internal/gatewayapi/zz_generated.deepcopy.go +++ b/internal/gatewayapi/zz_generated.deepcopy.go @@ -227,6 +227,17 @@ func (in *Resources) DeepCopyInto(out *Resources) { } } } + if in.SecurityPolicies != nil { + in, out := &in.SecurityPolicies, &out.SecurityPolicies + *out = make([]*apiv1alpha1.SecurityPolicy, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(apiv1alpha1.SecurityPolicy) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources. diff --git a/internal/message/types.go b/internal/message/types.go index 78cf3002bd0e..1825a8f033e1 100644 --- a/internal/message/types.go +++ b/internal/message/types.go @@ -81,10 +81,12 @@ type PolicyStatuses struct { ClientTrafficPolicyStatuses watchable.Map[types.NamespacedName, *egv1a1.ClientTrafficPolicyStatus] BackendTrafficPolicyStatuses watchable.Map[types.NamespacedName, *egv1a1.BackendTrafficPolicyStatus] EnvoyPatchPolicyStatuses watchable.Map[types.NamespacedName, *egv1a1.EnvoyPatchPolicyStatus] + SecurityPolicyStatuses watchable.Map[types.NamespacedName, *egv1a1.SecurityPolicyStatus] } func (p *PolicyStatuses) Close() { p.ClientTrafficPolicyStatuses.Close() + p.SecurityPolicyStatuses.Close() p.EnvoyPatchPolicyStatuses.Close() } diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 006a168d5faa..663eb80a2995 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -320,7 +320,20 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, _ reconcile.Reques // It will be recomputed by the gateway-api layer policy.Status = egv1a1.BackendTrafficPolicyStatus{} resourceTree.BackendTrafficPolicies = append(resourceTree.BackendTrafficPolicies, &policy) + } + + // Add all SecurityPolicies + securityPolicies := egv1a1.SecurityPolicyList{} + if err := r.client.List(ctx, &securityPolicies); err != nil { + return reconcile.Result{}, fmt.Errorf("error listing SecurityPolicies: %v", err) + } + for _, policy := range securityPolicies.Items { + policy := policy + // Discard Status to reduce memory consumption in watchable + // It will be recomputed by the gateway-api layer + policy.Status = egv1a1.SecurityPolicyStatus{} + resourceTree.SecurityPolicies = append(resourceTree.SecurityPolicies, &policy) } // For this particular Gateway, and all associated objects, check whether the diff --git a/internal/status/securitypolicy.go b/internal/status/securitypolicy.go new file mode 100644 index 000000000000..ee3cfebe53f6 --- /dev/null +++ b/internal/status/securitypolicy.go @@ -0,0 +1,20 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package status + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func SetSecurityPolicyCondition(c *egv1a1.SecurityPolicy, conditionType gwv1a2.PolicyConditionType, status metav1.ConditionStatus, reason gwv1a2.PolicyConditionReason, message string) { + cond := newCondition(string(conditionType), status, string(reason), message, time.Now(), c.Generation) + c.Status.Conditions = MergeConditions(c.Status.Conditions, cond) +} diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 1f3545304b6e..7858248d692b 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -24,6 +24,8 @@ API group. - [EnvoyPatchPolicyList](#envoypatchpolicylist) - [EnvoyProxy](#envoyproxy) - [RateLimitFilter](#ratelimitfilter) +- [SecurityPolicy](#securitypolicy) +- [SecurityPolicyList](#securitypolicylist) @@ -1446,6 +1448,55 @@ _Appears in:_ +#### SecurityPolicy + + + +SecurityPolicy allows the user to configure various security settings for a Gateway. + +_Appears in:_ +- [SecurityPolicyList](#securitypolicylist) + +| Field | Description | +| --- | --- | +| `apiVersion` _string_ | `gateway.envoyproxy.io/v1alpha1` +| `kind` _string_ | `SecurityPolicy` +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` _[SecurityPolicySpec](#securitypolicyspec)_ | Spec defines the desired state of SecurityPolicy. | + + +#### SecurityPolicyList + + + +SecurityPolicyList contains a list of SecurityPolicy resources. + + + +| Field | Description | +| --- | --- | +| `apiVersion` _string_ | `gateway.envoyproxy.io/v1alpha1` +| `kind` _string_ | `SecurityPolicyList` +| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` _[SecurityPolicy](#securitypolicy) array_ | | + + +#### SecurityPolicySpec + + + +SecurityPolicySpec defines the desired state of SecurityPolicy. + +_Appears in:_ +- [SecurityPolicy](#securitypolicy) + +| Field | Description | +| --- | --- | +| `targetRef` _[PolicyTargetReferenceWithSectionName](#policytargetreferencewithsectionname)_ | TargetRef is the name of the Gateway resource this policy is being attached to. This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway. TargetRef | + + + + #### ServiceType _Underlying type:_ `string`