diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go new file mode 100644 index 00000000000..f53d237323a --- /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 c9b8600c45f..917d79ce680 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/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml new file mode 100644 index 00000000000..5e1418bf4ae --- /dev/null +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -0,0 +1,180 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: securitypolicies.gateway.envoyproxy.io +spec: + group: gateway.envoyproxy.io + names: + kind: SecurityPolicy + listKind: SecurityPolicyList + plural: securitypolicies + singular: securitypolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].reason + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: SecurityPolicy allows the user to configure various security + settings for a Gateway. + 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 SecurityPolicy. + properties: + targetRef: + description: 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 + 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 + required: + - targetRef + type: object + status: + description: Status defines the current status of SecurityPolicy. + properties: + conditions: + description: Conditions describe the current conditions of the SecurityPolicy. + 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 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/gateway-helm/templates/_rbac.tpl b/charts/gateway-helm/templates/_rbac.tpl index 5d0c4cc846c..f0107bb6fe1 100644 --- a/charts/gateway-helm/templates/_rbac.tpl +++ b/charts/gateway-helm/templates/_rbac.tpl @@ -68,6 +68,7 @@ resources: - envoypatchpolicies - clienttrafficpolicies - backendtrafficpolicies +- securitypolicies - ratelimitfilters verbs: - get diff --git a/internal/gatewayapi/resource.go b/internal/gatewayapi/resource.go index 6b0e87303d7..318728afce7 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 9a89ba48eb4..941286b2f58 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 00000000000..24ae703e6b3 --- /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 00000000000..bdc74d838b1 --- /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 00000000000..d4c17232a74 --- /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 ae8279a8d4c..b66585f235e 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 6e5d0fa0529..fb847507b0a 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 78cf3002bd0..1825a8f033e 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 006a168d5fa..663eb80a299 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 00000000000..ee3cfebe53f --- /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 1f3545304b6..7858248d692 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`