diff --git a/go.mod b/go.mod index 1094823d5..18cc8b3df 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.24.6 require ( github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 + github.com/envoyproxy/go-control-plane v0.13.1 github.com/gardener/gardener v1.117.6 github.com/gardener/gardener-extension-provider-openstack v1.47.0 github.com/go-logr/logr v1.4.3 @@ -16,7 +17,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 golang.org/x/tools v0.34.0 - gopkg.in/yaml.v3 v3.0.1 + google.golang.org/protobuf v1.36.5 istio.io/api v1.25.2 istio.io/client-go v1.25.1 k8s.io/api v0.32.7 @@ -28,9 +29,11 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231015215740-bf15e44028f9 + sigs.k8s.io/yaml v1.4.0 ) require ( + cel.dev/expr v0.19.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -39,10 +42,12 @@ require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fluent/fluent-operator/v3 v3.3.0 // indirect @@ -87,6 +92,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -116,10 +122,11 @@ require ( golang.org/x/time v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v3 v3.17.3 // indirect k8s.io/autoscaler v0.0.0-20190805135949-100e91ba756e // indirect k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect @@ -133,5 +140,4 @@ require ( sigs.k8s.io/controller-tools v0.17.3 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 19e956871..47c634750 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= +cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -58,6 +60,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -76,7 +80,11 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= +github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -285,6 +293,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -531,6 +541,8 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/pkg/controller/actuator.go b/pkg/controller/actuator.go index 94f73194a..2aeae16c6 100644 --- a/pkg/controller/actuator.go +++ b/pkg/controller/actuator.go @@ -297,10 +297,14 @@ func (a *actuator) createSeedResources( // The `nginx-ingress-controller` Gateway object only exists in g/g@v1.89, (introduced with // https://github.com/gardener/gardener/pull/9038). // If it doesn't exist yet, we can't apply ACLs to shoot ingresses. - ingressEnvoyFilterSpec := envoyfilters.BuildIngressEnvoyFilterSpecForHelmChart( + ingressEnvoyFilterSpec, err := envoyfilters.BuildIngressEnvoyFilterSpecForHelmChart( cluster, spec.Rule, alwaysAllowedCIDRs, defaultLabels) - - cfg["ingressEnvoyFilterSpec"] = ingressEnvoyFilterSpec + if err != nil { + return err + } + if ingressEnvoyFilterSpec != nil { + cfg["ingressEnvoyFilterSpec"] = ingressEnvoyFilterSpec + } } cfg, err = chart.InjectImages(cfg, imagevector.ImageVector(), []string{ImageName}) diff --git a/pkg/envoyfilters/envoyfilters.go b/pkg/envoyfilters/envoyfilters.go index 79619d6a3..9fff0daf1 100644 --- a/pkg/envoyfilters/envoyfilters.go +++ b/pkg/envoyfilters/envoyfilters.go @@ -2,10 +2,22 @@ package envoyfilters import ( "errors" + "fmt" "net" "strings" + envoy_corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_rbacv3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + envoy_routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_httprbacv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" + envoy_networkrbacv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3" + envoy_matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/gardener/gardener/extensions/pkg/controller" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/wrapperspb" + istio_networkingv1alpha3 "istio.io/api/networking/v1alpha3" "github.com/stackitcloud/gardener-extension-acl/pkg/helper" ) @@ -26,21 +38,59 @@ type ACLRule struct { Type string `json:"type"` } +func (r *ACLRule) actionProto() (envoy_rbacv3.RBAC_Action, error) { + switch r.Action { + case "DENY": + return envoy_rbacv3.RBAC_DENY, nil + case "ALLOW": + return envoy_rbacv3.RBAC_ALLOW, nil + default: + return -1, fmt.Errorf("unknown action %s", r.Action) + } +} + +// FilterPatch represents the object beneath EnvoyFilter.spec.configPatches.patch.value +// It holds the name of the filter and it's typed config to inject into the envoy config +type FilterPatch struct { + Name string `json:"name"` + TypedConfig *structpb.Struct `json:"typed_config"` +} + +// asStructPB returns FilterPatch represented as a structpb.Struct +func (f *FilterPatch) asStructPB() (*structpb.Struct, error) { + pb, err := structpb.NewStruct(map[string]any{ + "name": f.Name, + "typed_config": f.TypedConfig.AsMap(), + }) + if err != nil { + return nil, err + } + return pb, nil +} + +// AsMap returns FilterPatch represented as a map[string]interface{} +func (f *FilterPatch) AsMap() (map[string]interface{}, error) { + s, err := f.asStructPB() + if err != nil { + return nil, err + } + return s.AsMap(), nil +} + // BuildAPIEnvoyFilterSpecForHelmChart assembles EnvoyFilter patches for API server // networking for every rule in the extension spec. func BuildAPIEnvoyFilterSpecForHelmChart( rule *ACLRule, hosts, alwaysAllowedCIDRs []string, istioLabels map[string]string, -) (map[string]interface{}, error) { +) (*istio_networkingv1alpha3.EnvoyFilter, error) { apiConfigPatch, err := CreateAPIConfigPatchFromRule(rule, hosts, alwaysAllowedCIDRs) if err != nil { return nil, err } - - return map[string]interface{}{ - "workloadSelector": map[string]interface{}{ - "labels": istioLabels, + return &istio_networkingv1alpha3.EnvoyFilter{ + WorkloadSelector: &istio_networkingv1alpha3.WorkloadSelector{ + Labels: istioLabels, }, - "configPatches": []map[string]interface{}{ + ConfigPatches: []*istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ apiConfigPatch, }, }, nil @@ -50,193 +100,278 @@ func BuildAPIEnvoyFilterSpecForHelmChart( // endpoints using the seed ingress domain. func BuildIngressEnvoyFilterSpecForHelmChart( cluster *controller.Cluster, rule *ACLRule, alwaysAllowedCIDRs []string, istioLabels map[string]string, -) map[string]interface{} { +) (*istio_networkingv1alpha3.EnvoyFilter, error) { seedIngressDomain := helper.GetSeedIngressDomain(cluster.Seed) - if seedIngressDomain != "" { - shootID := helper.ComputeShortShootID(cluster.Shoot) - - return map[string]interface{}{ - "workloadSelector": map[string]interface{}{ - "labels": istioLabels, - }, - "configPatches": []map[string]interface{}{ - CreateIngressConfigPatchFromRule(rule, seedIngressDomain, shootID, alwaysAllowedCIDRs), - }, - } + if seedIngressDomain == "" { + return nil, nil } - return nil -} + shootID := helper.ComputeShortShootID(cluster.Shoot) -// BuildVPNEnvoyFilterSpecForHelmChart assembles EnvoyFilter patches for VPN. -func BuildVPNEnvoyFilterSpecForHelmChart( - cluster *controller.Cluster, rule *ACLRule, alwaysAllowedCIDRs []string, istioLabels map[string]string, -) (map[string]interface{}, error) { - vpnConfigPatch, err := CreateVPNConfigPatchFromRule(rule, helper.ComputeShortShootID(cluster.Shoot), cluster.Shoot.Status.TechnicalID, alwaysAllowedCIDRs) + configPatch, err := ingressConfigPatchFromRule(rule, seedIngressDomain, shootID, alwaysAllowedCIDRs) if err != nil { return nil, err } - - return map[string]interface{}{ - "workloadSelector": map[string]interface{}{ - "labels": istioLabels, - }, - "configPatches": []map[string]interface{}{ - vpnConfigPatch, - }, - }, nil -} - -// CreateAPIConfigPatchFromRule combines an ACLRule, the first entry of the -// hosts list and the alwaysAllowedCIDRs into a network filter patch that can be -// applied to the `GATEWAY` network filter chain matching the host. -func CreateAPIConfigPatchFromRule( - rule *ACLRule, hosts, alwaysAllowedCIDRs []string, -) (map[string]interface{}, error) { - if len(hosts) == 0 { - return nil, ErrNoHostsGiven - } - rbacName := "acl-api" - principals := ruleCIDRsToPrincipal(rule, alwaysAllowedCIDRs) - - return map[string]interface{}{ - "applyTo": "NETWORK_FILTER", - "match": map[string]interface{}{ - "context": "GATEWAY", - "listener": map[string]interface{}{ - "filterChain": map[string]interface{}{ - // There is one filter chain per shoot in the SNI listener that has two SNI matches: one for the internal and - // one for the external shoot domain. - // We can use either shoot domain to match the filter chain that we want to patch with this EnvoyFilter. - // The ACL config will apply to traffic going via both the internal and the external API server address. - // See: https://istio.io/latest/docs/reference/config/networking/envoy-filter/#EnvoyFilter-ListenerMatch-FilterChainMatch - "sni": hosts[0], - }, - }, + return &istio_networkingv1alpha3.EnvoyFilter{ + WorkloadSelector: &istio_networkingv1alpha3.WorkloadSelector{ + Labels: istioLabels, }, - "patch": principalsToPatch(rbacName, rule.Action, "network", principals), + ConfigPatches: []*istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{configPatch}, }, nil } -// CreateIngressConfigPatchFromRule creates a network filter patch that can be +// ingressConfigPatchFromRule creates a network filter patch that can be // applied to the `GATEWAY` network filter chain matching the wildcard ingress domain. -func CreateIngressConfigPatchFromRule( +func ingressConfigPatchFromRule( rule *ACLRule, seedIngressDomain, shootID string, alwaysAllowedCIDRs []string, -) map[string]interface{} { +) (*istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { rbacName := "acl-ingress" ingressSuffix := "-" + shootID + "." + seedIngressDomain - return map[string]interface{}{ - "applyTo": "NETWORK_FILTER", - "match": map[string]interface{}{ - "context": "GATEWAY", - "listener": map[string]interface{}{ - "filterChain": map[string]interface{}{ - "sni": "*." + seedIngressDomain, - }, - }, - }, - "patch": map[string]interface{}{ - "operation": "INSERT_FIRST", - "value": map[string]interface{}{ - "name": rbacName, - "typed_config": map[string]interface{}{ - "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", - "rules": map[string]interface{}{ - "action": "ALLOW", - "policies": map[string]interface{}{ - shootID + "-inverse": map[string]interface{}{ - "permissions": []map[string]interface{}{{ - "not_rule": map[string]interface{}{ - "requested_server_name": map[string]interface{}{ - "suffix": ingressSuffix, + rbacFilter := &envoy_networkrbacv3.RBAC{ + StatPrefix: "envoyrbac", + Rules: &envoy_rbacv3.RBAC{ + Action: envoy_rbacv3.RBAC_ALLOW, + Policies: map[string]*envoy_rbacv3.Policy{ + shootID + "-inverse": { + Permissions: []*envoy_rbacv3.Permission{ + { + Rule: &envoy_rbacv3.Permission_NotRule{ + NotRule: &envoy_rbacv3.Permission{ + Rule: &envoy_rbacv3.Permission_RequestedServerName{ + RequestedServerName: &envoy_matcherv3.StringMatcher{ + MatchPattern: &envoy_matcherv3.StringMatcher_Suffix{ + Suffix: ingressSuffix, + }, }, }, - }}, - "principals": []map[string]interface{}{{ - "remote_ip": map[string]interface{}{ - "address_prefix": "0.0.0.0", - "prefix_len": 0, - }, - }}, + }, + }, + }, + }, + Principals: []*envoy_rbacv3.Principal{ + { + Identifier: &envoy_rbacv3.Principal_RemoteIp{ + RemoteIp: &envoy_corev3.CidrRange{ + AddressPrefix: "0.0.0.0", + PrefixLen: wrapperspb.UInt32(0), + }, }, - shootID: map[string]interface{}{ - "permissions": []map[string]interface{}{{ - "requested_server_name": map[string]interface{}{ - "suffix": ingressSuffix, + }, + }, + }, + shootID: { + Permissions: []*envoy_rbacv3.Permission{ + { + Rule: &envoy_rbacv3.Permission_RequestedServerName{ + RequestedServerName: &envoy_matcherv3.StringMatcher{ + MatchPattern: &envoy_matcherv3.StringMatcher_Suffix{ + Suffix: ingressSuffix, }, - }}, - "principals": ruleCIDRsToPrincipal(rule, alwaysAllowedCIDRs), + }, }, }, }, - "stat_prefix": "envoyrbac", + Principals: ruleCIDRsToPrincipal(rule, alwaysAllowedCIDRs), + }, + }, + }, + } + typedConfig, err := protoMessageToTypedConfig(rbacFilter) + if err != nil { + return nil, err + } + filter := &FilterPatch{ + Name: rbacName, + TypedConfig: typedConfig, + } + pb, err := filter.asStructPB() + if err != nil { + return nil, err + } + return &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istio_networkingv1alpha3.EnvoyFilter_NETWORK_FILTER, + Match: &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: istio_networkingv1alpha3.EnvoyFilter_GATEWAY, + ObjectTypes: &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istio_networkingv1alpha3.EnvoyFilter_ListenerMatch{ + FilterChain: &istio_networkingv1alpha3.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Sni: fmt.Sprintf("*.%s", seedIngressDomain), + }, }, }, }, + Patch: &istio_networkingv1alpha3.EnvoyFilter_Patch{ + Operation: istio_networkingv1alpha3.EnvoyFilter_Patch_INSERT_FIRST, + Value: pb, + }, + }, nil +} + +// BuildVPNEnvoyFilterSpecForHelmChart assembles EnvoyFilter patches for VPN. +func BuildVPNEnvoyFilterSpecForHelmChart( + cluster *controller.Cluster, rule *ACLRule, alwaysAllowedCIDRs []string, istioLabels map[string]string, +) (*istio_networkingv1alpha3.EnvoyFilter, error) { + patch, err := vpnConfigPatchFromRule(rule, helper.ComputeShortShootID(cluster.Shoot), cluster.Shoot.Status.TechnicalID, alwaysAllowedCIDRs) + if err != nil { + return nil, err } + return &istio_networkingv1alpha3.EnvoyFilter{ + WorkloadSelector: &istio_networkingv1alpha3.WorkloadSelector{ + Labels: istioLabels, + }, + ConfigPatches: []*istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{patch}, + }, nil } -// CreateVPNConfigPatchFromRule creates an HTTP filter patch that can be applied to the +// vpnConfigPatchFromRule creates an HTTP filter patch that can be applied to the // `GATEWAY` HTTP filter chain for the VPN. -func CreateVPNConfigPatchFromRule(rule *ACLRule, +func vpnConfigPatchFromRule(rule *ACLRule, shortShootID, technicalShootID string, alwaysAllowedCIDRs []string, -) (map[string]interface{}, error) { +) (*istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { rbacName := "acl-vpn" - headerMatcher := map[string]interface{}{ - "name": "reversed-vpn", - "string_match": map[string]interface{}{ - // The actual header value will look something like - // `outbound|1194||vpn-seed-server..svc.cluster.local`. - // Include dots in the contains matcher as anchors, to always match the entire technical shoot ID. - // Otherwise, if there was one cluster named `foo` and one named `foo-bar` (in the same project), - // `foo` would effectively inherit the ACL of `foo-bar`. - // We don't match with the full header value to allow service names and ports to change while still making sure - // we catch all traffic targeting this shoot. - "contains": "." + technicalShootID + ".", - }, - } - return map[string]interface{}{ - "applyTo": "HTTP_FILTER", - "match": map[string]interface{}{ - "context": "GATEWAY", - "listener": map[string]interface{}{ - "name": "0.0.0.0_8132", + headerMatcher := envoy_routev3.HeaderMatcher{ + Name: "reversed-vpn", + HeaderMatchSpecifier: &envoy_routev3.HeaderMatcher_StringMatch{ + StringMatch: &envoy_matcherv3.StringMatcher{ + MatchPattern: &envoy_matcherv3.StringMatcher_Contains{ + // The actual header value will look something like + // `outbound|1194||vpn-seed-server..svc.cluster.local`. + // Include dots in the contains matcher as anchors, to always match the entire technical shoot ID. + // Otherwise, if there was one cluster named `foo` and one named `foo-bar` (in the same project), + // `foo` would effectively inherit the ACL of `foo-bar`. + // We don't match with the full header value to allow service names and ports to change while still making sure + // we catch all traffic targeting this shoot. + Contains: "." + technicalShootID + ".", + }, }, }, - "patch": map[string]interface{}{ - "operation": "INSERT_FIRST", - "value": map[string]interface{}{ - "name": rbacName, - "typed_config": map[string]interface{}{ - "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", - "rules": map[string]interface{}{ - "action": "ALLOW", - "policies": map[string]interface{}{ - shortShootID + "-inverse": map[string]interface{}{ - "permissions": []map[string]interface{}{{ - "not_rule": map[string]interface{}{ - "header": headerMatcher, - }, - }}, - "principals": []map[string]interface{}{{ - "remote_ip": map[string]interface{}{ - "address_prefix": "0.0.0.0", - "prefix_len": 0, + } + + rbacFilter := &envoy_httprbacv3.RBAC{ + RulesStatPrefix: "envoyrbac", + Rules: &envoy_rbacv3.RBAC{ + Action: envoy_rbacv3.RBAC_ALLOW, + Policies: map[string]*envoy_rbacv3.Policy{ + shortShootID + "-inverse": { + Permissions: []*envoy_rbacv3.Permission{ + { + Rule: &envoy_rbacv3.Permission_NotRule{ + NotRule: &envoy_rbacv3.Permission{ + Rule: &envoy_rbacv3.Permission_Header{ + Header: &headerMatcher, }, - }}, + }, + }, + }, + }, + Principals: []*envoy_rbacv3.Principal{ + { + Identifier: &envoy_rbacv3.Principal_RemoteIp{ + RemoteIp: &envoy_corev3.CidrRange{ + AddressPrefix: "0.0.0.0", + PrefixLen: wrapperspb.UInt32(0), + }, }, - shortShootID: map[string]interface{}{ - "permissions": []map[string]interface{}{{ - "header": headerMatcher, - }}, - "principals": ruleCIDRsToPrincipal(rule, alwaysAllowedCIDRs), + }, + }, + }, + shortShootID: { + Permissions: []*envoy_rbacv3.Permission{ + { + Rule: &envoy_rbacv3.Permission_Header{ + Header: &headerMatcher, }, }, }, - "stat_prefix": "envoyrbac", + Principals: ruleCIDRsToPrincipal(rule, alwaysAllowedCIDRs), + }, + }, + }, + } + typedConfig, err := protoMessageToTypedConfig(rbacFilter) + if err != nil { + return nil, err + } + filterPatch := &FilterPatch{ + Name: rbacName, + TypedConfig: typedConfig, + } + pb, err := filterPatch.asStructPB() + if err != nil { + return nil, err + } + + return &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istio_networkingv1alpha3.EnvoyFilter_HTTP_FILTER, + Match: &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: istio_networkingv1alpha3.EnvoyFilter_GATEWAY, + ObjectTypes: &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istio_networkingv1alpha3.EnvoyFilter_ListenerMatch{ + Name: "0.0.0.0_8132", + }, + }, + }, + Patch: &istio_networkingv1alpha3.EnvoyFilter_Patch{ + Operation: istio_networkingv1alpha3.EnvoyFilter_Patch_INSERT_FIRST, + Value: pb, + }, + }, nil +} + +// CreateAPIConfigPatchFromRule combines an ACLRule, the first entry of the +// hosts list and the alwaysAllowedCIDRs into a network filter patch that can be +// applied to the `GATEWAY` network filter chain matching the host. +func CreateAPIConfigPatchFromRule( + rule *ACLRule, hosts, alwaysAllowedCIDRs []string, +) (*istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { + if len(hosts) == 0 { + return nil, ErrNoHostsGiven + } + rbacName := "acl-api" + principals := ruleCIDRsToPrincipal(rule, alwaysAllowedCIDRs) + action, err := rule.actionProto() + if err != nil { + return nil, err + } + patch, err := principalsToPatch(rbacName, action, principals) + if err != nil { + return nil, err + } + return &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ + ApplyTo: istio_networkingv1alpha3.EnvoyFilter_NETWORK_FILTER, + Match: &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: istio_networkingv1alpha3.EnvoyFilter_GATEWAY, + ObjectTypes: &istio_networkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istio_networkingv1alpha3.EnvoyFilter_ListenerMatch{ + FilterChain: &istio_networkingv1alpha3.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Sni: hosts[0], + }, }, }, }, + Patch: patch, + }, nil +} + +func principalsToPatch( + rbacName string, ruleAction envoy_rbacv3.RBAC_Action, principals []*envoy_rbacv3.Principal, +) (*istio_networkingv1alpha3.EnvoyFilter_Patch, error) { + rbacFilter := newRBACFilter(rbacName, ruleAction, principals) + typedConfig, err := protoMessageToTypedConfig(rbacFilter) + if err != nil { + return nil, err + } + filter := &FilterPatch{ + Name: rbacName, + TypedConfig: typedConfig, + } + pb, err := filter.asStructPB() + if err != nil { + return nil, err + } + return &istio_networkingv1alpha3.EnvoyFilter_Patch{ + Operation: istio_networkingv1alpha3.EnvoyFilter_Patch_INSERT_FIRST, + Value: pb, }, nil } @@ -246,13 +381,22 @@ func CreateInternalFilterPatchFromRule( rule *ACLRule, alwaysAllowedCIDRs []string, shootSpecificCIDRs []string, -) (map[string]interface{}, error) { +) (*FilterPatch, error) { rbacName := "acl-internal" principals := ruleCIDRsToPrincipal(rule, append(alwaysAllowedCIDRs, shootSpecificCIDRs...)) + action, err := rule.actionProto() + if err != nil { + return nil, err + } + rbacFilter := newRBACFilter(rbacName, action, principals) + typedConfig, err := protoMessageToTypedConfig(rbacFilter) + if err != nil { + return nil, err + } - return map[string]interface{}{ - "name": rbacName + "-" + strings.ToLower(rule.Type), - "typed_config": typedConfigToPatch(rbacName, rule.Action, "network", principals), + return &FilterPatch{ + Name: rbacName + "-" + strings.ToLower(rule.Type), + TypedConfig: typedConfig, }, nil } @@ -260,20 +404,30 @@ func CreateInternalFilterPatchFromRule( // into a list of envoy principals. The function checks for the rule action: If // the action is "ALLOW", the alwaysAllowedCIDRs are appended to the principals // to guarantee the downstream flow for these CIDRs is not blocked. -func ruleCIDRsToPrincipal(rule *ACLRule, alwaysAllowedCIDRs []string) []map[string]interface{} { - principals := []map[string]interface{}{} +func ruleCIDRsToPrincipal(rule *ACLRule, alwaysAllowedCIDRs []string) []*envoy_rbacv3.Principal { + principals := []*envoy_rbacv3.Principal{} for _, cidr := range rule.Cidrs { prefix, length, err := getPrefixAndPrefixLength(cidr) if err != nil { continue } - principals = append(principals, map[string]interface{}{ - strings.ToLower(rule.Type): map[string]interface{}{ - "address_prefix": prefix, - "prefix_len": length, - }, - }) + cidrRange := envoy_corev3.CidrRange{ + AddressPrefix: prefix, + PrefixLen: wrapperspb.UInt32(uint32(length)), + } + p := new(envoy_rbacv3.Principal) + switch strings.ToLower(rule.Type) { + case "source_ip": + p.Identifier = &envoy_rbacv3.Principal_SourceIp{SourceIp: &cidrRange} + case "remote_ip": + p.Identifier = &envoy_rbacv3.Principal_RemoteIp{RemoteIp: &cidrRange} + case "direct_remote_ip": + p.Identifier = &envoy_rbacv3.Principal_DirectRemoteIp{DirectRemoteIp: &cidrRange} + default: + continue + } + principals = append(principals, p) } // if the rule has action "ALLOW" (which means "limit the access to only the @@ -285,10 +439,12 @@ func ruleCIDRsToPrincipal(rule *ACLRule, alwaysAllowedCIDRs []string) []map[stri if err != nil { continue } - principals = append(principals, map[string]interface{}{ - "remote_ip": map[string]interface{}{ - "address_prefix": prefix, - "prefix_len": length, + principals = append(principals, &envoy_rbacv3.Principal{ + Identifier: &envoy_rbacv3.Principal_RemoteIp{ + RemoteIp: &envoy_corev3.CidrRange{ + AddressPrefix: prefix, + PrefixLen: wrapperspb.UInt32(uint32(length)), + }, }, }) } @@ -309,32 +465,39 @@ func getPrefixAndPrefixLength(cidr string) (prefix string, prefixLen int, err er return ip.String(), prefixLen, nil } -func principalsToPatch( - rbacName, ruleAction, filterType string, principals []map[string]interface{}, -) map[string]interface{} { - return map[string]interface{}{ - "operation": "INSERT_FIRST", - "value": map[string]interface{}{ - "name": rbacName, - "typed_config": typedConfigToPatch(rbacName, ruleAction, filterType, principals), - }, - } -} - -func typedConfigToPatch(rbacName, ruleAction, filterType string, principals []map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "@type": "type.googleapis.com/envoy.extensions.filters." + filterType + ".rbac.v3.RBAC", - "stat_prefix": "envoyrbac", - "rules": map[string]interface{}{ - "action": strings.ToUpper(ruleAction), - "policies": map[string]interface{}{ - rbacName: map[string]interface{}{ - "permissions": []map[string]interface{}{ - {"any": true}, +func newRBACFilter(rbacName string, ruleAction envoy_rbacv3.RBAC_Action, principals []*envoy_rbacv3.Principal) *envoy_networkrbacv3.RBAC { + return &envoy_networkrbacv3.RBAC{ + StatPrefix: "envoyrbac", + Rules: &envoy_rbacv3.RBAC{ + Action: ruleAction, + Policies: map[string]*envoy_rbacv3.Policy{ + rbacName: { + Permissions: []*envoy_rbacv3.Permission{ + { + Rule: &envoy_rbacv3.Permission_Any{ + Any: true, + }, + }, }, - "principals": principals, + Principals: principals, }, }, }, } } + +func protoMessageToTypedConfig(m proto.Message) (*structpb.Struct, error) { + raw, err := protojson.MarshalOptions{ + UseProtoNames: true, + }.Marshal(m) + if err != nil { + return nil, err + } + s := new(structpb.Struct) + if err := protojson.Unmarshal(raw, s); err != nil { + return nil, err + } + typeName := fmt.Sprintf("type.googleapis.com/%s", proto.MessageName(m)) + s.Fields["@type"] = structpb.NewStringValue(typeName) + return s, nil +} diff --git a/pkg/envoyfilters/envoyfilters_test.go b/pkg/envoyfilters/envoyfilters_test.go index a4cec48ac..8c0b88fd8 100644 --- a/pkg/envoyfilters/envoyfilters_test.go +++ b/pkg/envoyfilters/envoyfilters_test.go @@ -1,6 +1,7 @@ package envoyfilters import ( + "encoding/json" "os" "path" @@ -8,8 +9,10 @@ import ( "github.com/gardener/gardener/pkg/extensions" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "gopkg.in/yaml.v3" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" ) var _ = Describe("EnvoyFilter Unit Tests", func() { @@ -52,7 +55,7 @@ var _ = Describe("EnvoyFilter Unit Tests", func() { result, err := BuildAPIEnvoyFilterSpecForHelmChart(rule, hosts, alwaysAllowedCIDRs, labels) Expect(err).ToNot(HaveOccurred()) - checkIfMapEqualsYAML(result, "apiEnvoyFilterSpecWithOneAllowRule.yaml") + checkIfFilterEquals(result, "apiEnvoyFilterSpecWithOneAllowRule.yaml") }) }) }) @@ -65,9 +68,10 @@ var _ = Describe("EnvoyFilter Unit Tests", func() { "app": "istio-ingressgateway", "istio": "ingressgateway", } - ingressEnvoyFilterSpec := BuildIngressEnvoyFilterSpecForHelmChart(cluster, rule, alwaysAllowedCIDRs, labels) + ingressEnvoyFilterSpec, err := BuildIngressEnvoyFilterSpecForHelmChart(cluster, rule, alwaysAllowedCIDRs, labels) + Expect(err).NotTo(HaveOccurred()) - checkIfMapEqualsYAML(ingressEnvoyFilterSpec, "ingressEnvoyFilterSpecWithOneAllowRule.yaml") + checkIfFilterEquals(ingressEnvoyFilterSpec, "ingressEnvoyFilterSpecWithOneAllowRule.yaml") }) It("Should not create an envoyFilter spec when seed has no ingress", func() { rule := createRule("ALLOW", "remote_ip", "10.180.0.0/16") @@ -76,8 +80,10 @@ var _ = Describe("EnvoyFilter Unit Tests", func() { "app": "istio-ingressgateway", "istio": "ingressgateway", } - ingressEnvoyFilterSpec := BuildIngressEnvoyFilterSpecForHelmChart(cluster, rule, alwaysAllowedCIDRs, labels) - Expect(ingressEnvoyFilterSpec["ingressEnvoyFilterSpec"]).To(BeNil()) + ingressEnvoyFilterSpec, err := BuildIngressEnvoyFilterSpecForHelmChart(cluster, rule, alwaysAllowedCIDRs, labels) + + Expect(err).NotTo(HaveOccurred()) + Expect(ingressEnvoyFilterSpec).To(BeNil()) }) }) }) @@ -92,8 +98,8 @@ var _ = Describe("EnvoyFilter Unit Tests", func() { } result, err := BuildVPNEnvoyFilterSpecForHelmChart(cluster, rule, alwaysAllowedCIDRs, labels) - Expect(err).ToNot(HaveOccurred()) - checkIfMapEqualsYAML(result, "vpnEnvoyFilterSpecWithOneAllowRule.yaml") + Expect(err).NotTo(HaveOccurred()) + checkIfFilterEquals(result, "vpnEnvoyFilterSpecWithOneAllowRule.yaml") }) }) }) @@ -104,9 +110,8 @@ var _ = Describe("EnvoyFilter Unit Tests", func() { rule := createRule("ALLOW", "remote_ip", "0.0.0.0/0") result, err := CreateInternalFilterPatchFromRule(rule, alwaysAllowedCIDRs, []string{}) - - Expect(err).ToNot(HaveOccurred()) - checkIfMapEqualsYAML(result, "singleFiltersAllowEntry.yaml") + Expect(err).NotTo(HaveOccurred()) + checkIfFilterEquals(result, "singleFiltersAllowEntry.yaml") }) }) }) @@ -123,7 +128,6 @@ var _ = Describe("EnvoyFilter Unit Tests", func() { }) }) }) - }) //nolint:unparam // action currently only accepts ALLOW but that might change, so we leave the parameterization @@ -137,19 +141,24 @@ func createRule(action, ruleType, cidr string) *ACLRule { } } -// checkIfMapEqualsYAML takes a map as input, and tries to compare its +// checkIfFilterEquals takes an object as input, and tries to compare its // marshaled contents to the string coming from the specified testdata file. // Fails the test if strings differ. The file contents are unmarshaled and // marshaled again to guarantee the strings are comparable. -func checkIfMapEqualsYAML(input map[string]interface{}, relTestingFilePath string) { - goldenYAMLByteArray, err := os.ReadFile(path.Join("./testdata", relTestingFilePath)) +func checkIfFilterEquals(input any, relTestingFilePath string) { + goldenYAMLBytes, err := os.ReadFile(path.Join("./testdata", relTestingFilePath)) Expect(err).ToNot(HaveOccurred()) - goldenMap := map[string]interface{}{} - Expect(yaml.Unmarshal(goldenYAMLByteArray, goldenMap)).To(Succeed()) - goldenYAMLProcessedByteArray, err := yaml.Marshal(goldenMap) + + goldenJSON, err := yaml.YAMLToJSON(goldenYAMLBytes) Expect(err).ToNot(HaveOccurred()) - inputByteArray, err := yaml.Marshal(input) + var actual []byte + if m, ok := input.(proto.Message); ok { + actual, err = protojson.Marshal(m) + } else { + actual, err = json.Marshal(input) + } Expect(err).ToNot(HaveOccurred()) - Expect(string(inputByteArray)).To(Equal(string(goldenYAMLProcessedByteArray))) + + Expect(actual).To(MatchJSON(goldenJSON)) } diff --git a/pkg/envoyfilters/testdata/apiEnvoyFilterSpecWithOneAllowRule.yaml b/pkg/envoyfilters/testdata/apiEnvoyFilterSpecWithOneAllowRule.yaml index 85ffc7681..f62e31e8c 100644 --- a/pkg/envoyfilters/testdata/apiEnvoyFilterSpecWithOneAllowRule.yaml +++ b/pkg/envoyfilters/testdata/apiEnvoyFilterSpecWithOneAllowRule.yaml @@ -12,7 +12,6 @@ configPatches: typed_config: '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC rules: - action: ALLOW policies: acl-api: permissions: @@ -31,4 +30,4 @@ configPatches: workloadSelector: labels: app: istio-ingressgateway - istio: ingressgateway \ No newline at end of file + istio: ingressgateway diff --git a/pkg/envoyfilters/testdata/ingressEnvoyFilterSpecWithOneAllowRule.yaml b/pkg/envoyfilters/testdata/ingressEnvoyFilterSpecWithOneAllowRule.yaml index 0a0f9442c..e5a8f14b6 100644 --- a/pkg/envoyfilters/testdata/ingressEnvoyFilterSpecWithOneAllowRule.yaml +++ b/pkg/envoyfilters/testdata/ingressEnvoyFilterSpecWithOneAllowRule.yaml @@ -12,7 +12,6 @@ configPatches: typed_config: '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC rules: - action: ALLOW policies: bar--foo: permissions: diff --git a/pkg/envoyfilters/testdata/legacyVPNEnvoyFilterSpecWithOneAllowRule.yaml b/pkg/envoyfilters/testdata/legacyVPNEnvoyFilterSpecWithOneAllowRule.yaml index 760815129..890903ed4 100644 --- a/pkg/envoyfilters/testdata/legacyVPNEnvoyFilterSpecWithOneAllowRule.yaml +++ b/pkg/envoyfilters/testdata/legacyVPNEnvoyFilterSpecWithOneAllowRule.yaml @@ -11,7 +11,6 @@ configPatches: typed_config: '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC rules: - action: ALLOW policies: acl-vpn-inverse: permissions: diff --git a/pkg/envoyfilters/testdata/singleFiltersAllowEntry.yaml b/pkg/envoyfilters/testdata/singleFiltersAllowEntry.yaml index feb69582e..b451bb8eb 100644 --- a/pkg/envoyfilters/testdata/singleFiltersAllowEntry.yaml +++ b/pkg/envoyfilters/testdata/singleFiltersAllowEntry.yaml @@ -2,7 +2,6 @@ name: acl-internal-remote_ip typed_config: '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC rules: - action: ALLOW policies: acl-internal: permissions: diff --git a/pkg/envoyfilters/testdata/vpnEnvoyFilterSpecWithOneAllowRule.yaml b/pkg/envoyfilters/testdata/vpnEnvoyFilterSpecWithOneAllowRule.yaml index 8a0f4db9b..86b24bb44 100644 --- a/pkg/envoyfilters/testdata/vpnEnvoyFilterSpecWithOneAllowRule.yaml +++ b/pkg/envoyfilters/testdata/vpnEnvoyFilterSpecWithOneAllowRule.yaml @@ -11,7 +11,6 @@ configPatches: typed_config: '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC rules: - action: ALLOW policies: bar--foo-inverse: permissions: @@ -40,7 +39,7 @@ configPatches: - remote_ip: address_prefix: 10.96.0.0 prefix_len: 11 - stat_prefix: envoyrbac + rules_stat_prefix: envoyrbac workloadSelector: labels: app: istio-ingressgateway