Repository: kumahq/kuma
Pull request #5009 (2022-09-14 22:41:36)

feat(*): xds generator for MeshTrafficPermission
### Checklist prior to review

<!--
Each of these sections need to be filled by the author when opening the PR.

If something doesn't apply please check the box and add a justification after the `--`
-->

- [X] Link to docs PR or issue -- N/A
- [X] Link to UI issue or PR -- N/A
- [X] Is the [issue worked on linked][1]? -- https://github.com/kumahq/kuma/issues/4992
- [X] The PR does not hardcode values that might break projects that depend on kuma (e.g. "kumahq" as a image registry) --
- [X]  The PR will work for both Linux and Windows, system specific functions like `syscall.Mkfifo` have equivalent implementation on the other OS --
- [X] Unit Tests --
- [X] E2E Tests --
- [X] Manual Universal Tests --
- [X] Manual Kubernetes Tests --
- [X] Do you need to update [`UPGRADE.md`](../blob/master/UPGRADE.md)? -- No
- [X] Does it need to be backported according to the [backporting policy](../blob/master/CONTRIBUTING.md#backporting)? -- No
- [X] Do you need to explicitly set a [`> Changelog:` entry here](../blob/master/CONTRIBUTING.md#submitting-a-patch)? No

[1]: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword


lahabana: @lobkovilya can you rebase this?
lobkovilya: @lahabana I merged `master` yesterday, I think it didn't change since then
lahabana: Yes sorry noticed after...

Patch:
diff --git a/docs/madr/decisions/007-mesh-traffic-permission.md b/docs/madr/decisions/007-mesh-traffic-permission.md
index 39989c154..8152a0a00 100644
--- a/docs/madr/decisions/007-mesh-traffic-permission.md
+++ b/docs/madr/decisions/007-mesh-traffic-permission.md
@@ -473,7 +473,7 @@ List of principals generated based on the list from step 3. Each principal is ma
 ```yaml
 action: ALLOW
 policies:
-  "ShadowMeshTrafficPermission":
+  "MeshTrafficPermission":
     permissions:
       - any: true
     principals:
diff --git a/mk/generate.mk b/mk/generate.mk
index 4d5c6f920..8faf9630a 100644
--- a/mk/generate.mk
+++ b/mk/generate.mk
@@ -45,7 +45,7 @@ cleanup/crds:
 
 # deletes all files in policy directory except *.proto and validator.go
 cleanup/policy/%:
-	$(shell find $(POLICIES_DIR)/$* \( -name '*.pb.go' -o -name '*.yaml' -o -name 'zz_generated.*'  \) -type f -delete)
+	$(shell find $(POLICIES_DIR)/$* \( -name '*.pb.go' -o -name '*.yaml' -o -name 'zz_generated.*'  \) -not -path '*/testdata/*' -type f -delete)
 	@rm -fr $(POLICIES_DIR)/$*/k8s
 
 generate/policy/%: generate/schema/%
diff --git a/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1/meshtrafficpermission_helpers.go b/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1/meshtrafficpermission_helpers.go
new file mode 100644
index 000000000..27f95fe29
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1/meshtrafficpermission_helpers.go
@@ -0,0 +1,9 @@
+package v1alpha1
+
+func (x *MeshTrafficPermission_Conf) GetActionEnum() MeshTrafficPermission_Conf_Action {
+	return MeshTrafficPermission_Conf_Action(MeshTrafficPermission_Conf_Action_value[x.Action])
+}
+
+func (x *MeshTrafficPermission_Conf) SetActionEnum(e MeshTrafficPermission_Conf_Action) {
+	x.Action = MeshTrafficPermission_Conf_Action_name[int32(e)]
+}
diff --git a/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin.go b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin.go
index ff51bce07..01ca17489 100644
--- a/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin.go
+++ b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin.go
@@ -1,13 +1,18 @@
 package v1alpha1
 
 import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+
 	"github.com/kumahq/kuma/pkg/core"
 	core_plugins "github.com/kumahq/kuma/pkg/core/plugins"
 	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/plugins/policies/matchers"
 	api "github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1"
+	v3 "github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/xds"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/generator"
 )
 
 var _ core_plugins.PolicyPlugin = &plugin{}
@@ -25,6 +30,51 @@ func (p plugin) MatchedPolicies(dataplane *core_mesh.DataplaneResource, resource
 }
 
 func (p plugin) Apply(rs *core_xds.ResourceSet, ctx xds_context.Context, proxy *core_xds.Proxy) error {
-	log.Info("apply is not implemented")
+	if proxy.Dataplane == nil {
+		// MeshTrafficPermission policy is applied only on DPP
+		// todo(lobkovilya): add support for ExternalService and ZoneEgress, https://github.com/kumahq/kuma/issues/5050
+		return nil
+	}
+
+	mtp, ok := proxy.Policies.Dynamic[api.MeshTrafficPermissionType]
+	if !ok {
+		return nil
+	}
+
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		log.V(1).Info("skip applying MeshTrafficPermission, MTLS is disabled",
+			"proxyName", proxy.Dataplane.GetMeta().GetName(),
+			"mesh", ctx.Mesh.Resource.GetMeta().GetName())
+		return nil
+	}
+
+	for _, res := range rs.Resources(envoy_resource.ListenerType) {
+		if res.Origin != generator.OriginInbound {
+			continue
+		}
+
+		listener := res.Resource.(*envoy_listener.Listener)
+		dpAddress := listener.GetAddress().GetSocketAddress()
+
+		key := core_xds.InboundListener{
+			Address: dpAddress.GetAddress(),
+			Port:    dpAddress.GetPortValue(),
+		}
+		rules, ok := mtp.FromRules.Rules[key]
+		if !ok {
+			continue
+		}
+
+		configurer := &v3.RBACConfigurer{
+			StatsName: res.Name,
+			Rules:     rules,
+			Mesh:      proxy.Dataplane.GetMeta().GetMesh(),
+		}
+		for _, filterChain := range listener.FilterChains {
+			if err := configurer.Configure(filterChain); err != nil {
+				return err
+			}
+		}
+	}
 	return nil
 }
diff --git a/pkg/plugins/policies/meshtrafficpermission/xds/rbac_configurer.go b/pkg/plugins/policies/meshtrafficpermission/xds/rbac_configurer.go
new file mode 100644
index 000000000..ca2335c78
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/xds/rbac_configurer.go
@@ -0,0 +1,186 @@
+package xds
+
+import (
+	"fmt"
+
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	rbac_config "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
+	rbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
+	matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	policies_api "github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	tls "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+)
+
+type RBACConfigurer struct {
+	StatsName string
+	Rules     core_xds.Rules
+	Mesh      string
+}
+
+func (c *RBACConfigurer) Configure(filterChain *envoy_listener.FilterChain) error {
+	filter, err := c.createRBACFilter()
+	if err != nil {
+		return err
+	}
+
+	for idx, filter := range filterChain.Filters {
+		if filter.GetName() == "envoy.filters.network.rbac" {
+			// new MeshTrafficPermission takes over this filter chain,
+			// it's safe to delete RBAC from old TrafficPermissions
+			filterChain.Filters = append(filterChain.Filters[:idx], filterChain.Filters[idx+1:]...)
+			break
+		}
+	}
+
+	// RBAC filter should be the first in the chain
+	filterChain.Filters = append([]*envoy_listener.Filter{filter}, filterChain.Filters...)
+	return nil
+}
+
+func (c *RBACConfigurer) createRBACFilter() (*envoy_listener.Filter, error) {
+	// 'rules' always RBAC with 'action: ALLOW' regardless the number of principals
+	rules := &rbac_config.RBAC{
+		Action:   rbac_config.RBAC_ALLOW,
+		Policies: map[string]*rbac_config.Policy{},
+	}
+	if principals := c.createPrincipals(false); len(principals) != 0 {
+		rules.Policies["MeshTrafficPermission"] = &rbac_config.Policy{
+			Permissions: []*rbac_config.Permission{
+				{
+					Rule: &rbac_config.Permission_Any{
+						Any: true,
+					},
+				},
+			},
+			Principals: principals,
+		}
+	}
+
+	// 'shadowRules' could be nil if there are no shadow principals
+	var shadowRules *rbac_config.RBAC
+	if shadowPrincipals := c.createPrincipals(true); len(shadowPrincipals) != 0 {
+		shadowRules = &rbac_config.RBAC{
+			Action: rbac_config.RBAC_ALLOW,
+			Policies: map[string]*rbac_config.Policy{
+				"MeshTrafficPermission": {
+					Permissions: []*rbac_config.Permission{
+						{
+							Rule: &rbac_config.Permission_Any{
+								Any: true,
+							},
+						},
+					},
+					Principals: shadowPrincipals,
+				},
+			},
+		}
+	}
+
+	rbacMarshalled, err := util_proto.MarshalAnyDeterministic(&rbac.RBAC{
+		// we include dot to change "inbound:127.0.0.1:21011rbac.allowed" metric to "inbound:127.0.0.1:21011.rbac.allowed"
+		StatPrefix:  fmt.Sprintf("%s.", util_xds.SanitizeMetric(c.StatsName)),
+		Rules:       rules,
+		ShadowRules: shadowRules,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_listener.Filter{
+		// todo(lobkovilya): use 'envoy.filters.http.rbac' for HTTP traffic to have proper stats
+		Name: "envoy.filters.network.rbac",
+		ConfigType: &envoy_listener.Filter_TypedConfig{
+			TypedConfig: rbacMarshalled,
+		},
+	}, nil
+}
+
+func (c *RBACConfigurer) createPrincipals(shadow bool) []*rbac_config.Principal {
+	allowSubset := []core_xds.Subset{}
+	for _, rule := range c.Rules {
+		if rule.Conf == nil {
+			// the default action when no conf provided is DENY
+			continue
+		}
+		action := rule.Conf.(*policies_api.MeshTrafficPermission_Conf).GetActionEnum()
+		if shouldGenerateRuleForAction(action, shadow) {
+			allowSubset = append(allowSubset, rule.Subset)
+		}
+	}
+
+	principals := []*rbac_config.Principal{}
+	for _, ss := range allowSubset {
+		principals = append(principals, c.principalFromSubset(ss, c.Mesh))
+	}
+
+	return principals
+}
+
+func shouldGenerateRuleForAction(
+	action policies_api.MeshTrafficPermission_Conf_Action,
+	shadow bool,
+) bool {
+	if shadow {
+		return action == policies_api.MeshTrafficPermission_Conf_DENY_WITH_SHADOW_ALLOW
+	} else {
+		return action == policies_api.MeshTrafficPermission_Conf_ALLOW ||
+			action == policies_api.MeshTrafficPermission_Conf_ALLOW_WITH_SHADOW_DENY
+	}
+}
+
+func (c *RBACConfigurer) principalFromSubset(ss core_xds.Subset, mesh string) *rbac_config.Principal {
+	principals := []*rbac_config.Principal{}
+
+	for _, t := range ss {
+		var principalName *matcherv3.StringMatcher
+		switch t.Key {
+		case mesh_proto.ServiceTag:
+			service := t.Value
+			principalName = tls.ServiceSpiffeIDMatcher(mesh, service)
+		default:
+			principalName = tls.KumaIDMatcher(t.Key, t.Value)
+		}
+		principal := &rbac_config.Principal{
+			Identifier: &rbac_config.Principal_Authenticated_{
+				Authenticated: &rbac_config.Principal_Authenticated{
+					PrincipalName: principalName,
+				},
+			},
+		}
+		if t.Not {
+			principal = c.not(principal)
+		}
+		principals = append(principals, principal)
+	}
+
+	switch len(principals) {
+	case 0:
+		return &rbac_config.Principal{
+			Identifier: &rbac_config.Principal_Any{
+				Any: true,
+			},
+		}
+	case 1:
+		return principals[0]
+	default:
+		return &rbac_config.Principal{
+			Identifier: &rbac_config.Principal_AndIds{
+				AndIds: &rbac_config.Principal_Set{
+					Ids: principals,
+				},
+			},
+		}
+	}
+}
+
+func (c *RBACConfigurer) not(p *rbac_config.Principal) *rbac_config.Principal {
+	return &rbac_config.Principal{
+		Identifier: &rbac_config.Principal_NotId{
+			NotId: p,
+		},
+	}
+}


Test patch:
diff --git a/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin_test.go b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin_test.go
new file mode 100644
index 000000000..2522347e7
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/plugin_test.go
@@ -0,0 +1,130 @@
+package v1alpha1_test
+
+import (
+	"path"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	"github.com/kumahq/kuma/pkg/core/plugins"
+	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	policies_api "github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1"
+	meshtrafficpermission "github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1"
+	"github.com/kumahq/kuma/pkg/test/matchers"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/listeners"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+)
+
+var _ = Describe("Apply", func() {
+
+	It("should enrich matching listener with RBAC filter", func() {
+		// given
+		rs := core_xds.NewResourceSet()
+
+		// listener that matches
+		listener, err := listeners.NewListenerBuilder(envoy.APIV3).
+			Configure(listeners.InboundListener("test_listener", "192.168.0.1", 8080, core_xds.SocketAddressProtocolTCP)).
+			Configure(listeners.FilterChain(listeners.NewFilterChainBuilder(envoy.APIV3).
+				Configure(listeners.HttpConnectionManager("test_listener", false)))).
+			Build()
+		Expect(err).ToNot(HaveOccurred())
+		rs.Add(&core_xds.Resource{
+			Name:     listener.GetName(),
+			Origin:   generator.OriginInbound,
+			Resource: listener,
+		})
+
+		// listener that is originated from inbound proxy generator but won't match
+		listener2, err := listeners.NewListenerBuilder(envoy.APIV3).
+			Configure(listeners.InboundListener("test_listener2", "192.168.0.1", 8081, core_xds.SocketAddressProtocolTCP)).
+			Configure(listeners.FilterChain(listeners.NewFilterChainBuilder(envoy.APIV3).
+				Configure(listeners.HttpConnectionManager("test_listener2", false)))).
+			Build()
+		Expect(err).ToNot(HaveOccurred())
+		rs.Add(&core_xds.Resource{
+			Name:     listener2.GetName(),
+			Origin:   generator.OriginInbound,
+			Resource: listener2,
+		})
+
+		// listener that matches but is not originated from inbound proxy generator
+		listener3, err := listeners.NewListenerBuilder(envoy.APIV3).
+			Configure(listeners.InboundListener("test_listener3", "192.168.0.1", 8082, core_xds.SocketAddressProtocolTCP)).
+			Configure(listeners.FilterChain(listeners.NewFilterChainBuilder(envoy.APIV3).
+				Configure(listeners.HttpConnectionManager("test_listener3", false)))).
+			Build()
+		Expect(err).ToNot(HaveOccurred())
+		rs.Add(&core_xds.Resource{
+			Name:     listener3.GetName(),
+			Origin:   "not-inbound-origin",
+			Resource: listener3,
+		})
+
+		// mesh with enabled mTLS
+		ctx := xds_context.Context{
+			Mesh: xds_context.MeshContext{
+				Resource: &mesh.MeshResource{
+					Meta: &test_model.ResourceMeta{Name: "mesh-1", Mesh: core_model.NoMesh},
+					Spec: &mesh_proto.Mesh{
+						Mtls: &mesh_proto.Mesh_Mtls{
+							EnabledBackend: "builtin-1",
+							Backends: []*mesh_proto.CertificateAuthorityBackend{
+								{
+									Name: "builtin-1",
+									Type: "builtin",
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		proxy := &core_xds.Proxy{
+			Dataplane: &mesh.DataplaneResource{
+				Meta: &test_model.ResourceMeta{Name: "dp1", Mesh: "mesh-1"},
+			},
+			Policies: core_xds.MatchedPolicies{
+				Dynamic: map[core_model.ResourceType]core_xds.TypedMatchingPolicies{
+					policies_api.MeshTrafficPermissionType: {
+						FromRules: core_xds.FromRules{
+							Rules: map[core_xds.InboundListener]core_xds.Rules{
+								{
+									Address: "192.168.0.1", Port: 8080,
+								}: {
+									{
+										Subset: []core_xds.Tag{
+											{Key: mesh_proto.ServiceTag, Value: "frontend"},
+										},
+										Conf: &policies_api.MeshTrafficPermission_Conf{
+											Action: "ALLOW",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		// when
+		p := meshtrafficpermission.NewPlugin().(plugins.PolicyPlugin)
+		err = p.Apply(rs, ctx, proxy)
+		Expect(err).ToNot(HaveOccurred())
+
+		// then
+		resp, err := rs.List().ToDeltaDiscoveryResponse()
+		Expect(err).ToNot(HaveOccurred())
+		bytes, err := util_proto.ToYAML(resp)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(bytes).To(matchers.MatchGoldenYAML(path.Join("testdata", "apply.golden.yaml")))
+	})
+})
diff --git a/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/suite_test.go b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/suite_test.go
new file mode 100644
index 000000000..f65966485
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/suite_test.go
@@ -0,0 +1,11 @@
+package v1alpha1_test
+
+import (
+	"testing"
+
+	"github.com/kumahq/kuma/pkg/test"
+)
+
+func TestPlugin(t *testing.T) {
+	test.RunSpecs(t, "Plugin Suite")
+}
diff --git a/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/testdata/apply.golden.yaml b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/testdata/apply.golden.yaml
new file mode 100644
index 000000000..306c3f363
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/plugin/v1alpha1/testdata/apply.golden.yaml
@@ -0,0 +1,74 @@
+resources:
+- name: test_listener
+  resource:
+    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+    address:
+      socketAddress:
+        address: 192.168.0.1
+        portValue: 8080
+    enableReusePort: false
+    filterChains:
+    - filters:
+      - name: envoy.filters.network.rbac
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+          rules:
+            policies:
+              MeshTrafficPermission:
+                permissions:
+                - any: true
+                principals:
+                - authenticated:
+                    principalName:
+                      exact: spiffe://mesh-1/frontend
+          statPrefix: test_listener.
+      - name: envoy.filters.network.http_connection_manager
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+          httpFilters:
+          - name: envoy.filters.http.router
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+          statPrefix: test_listener
+    name: test_listener
+    trafficDirection: INBOUND
+- name: test_listener2
+  resource:
+    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+    address:
+      socketAddress:
+        address: 192.168.0.1
+        portValue: 8081
+    enableReusePort: false
+    filterChains:
+    - filters:
+      - name: envoy.filters.network.http_connection_manager
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+          httpFilters:
+          - name: envoy.filters.http.router
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+          statPrefix: test_listener2
+    name: test_listener2
+    trafficDirection: INBOUND
+- name: test_listener3
+  resource:
+    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+    address:
+      socketAddress:
+        address: 192.168.0.1
+        portValue: 8082
+    enableReusePort: false
+    filterChains:
+    - filters:
+      - name: envoy.filters.network.http_connection_manager
+        typedConfig:
+          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+          httpFilters:
+          - name: envoy.filters.http.router
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+          statPrefix: test_listener3
+    name: test_listener3
+    trafficDirection: INBOUND
diff --git a/pkg/plugins/policies/meshtrafficpermission/xds/rbac_configurer_test.go b/pkg/plugins/policies/meshtrafficpermission/xds/rbac_configurer_test.go
new file mode 100644
index 000000000..d8ff5e7b2
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/xds/rbac_configurer_test.go
@@ -0,0 +1,256 @@
+package xds_test
+
+import (
+	listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/api/v1alpha1"
+	"github.com/kumahq/kuma/pkg/plugins/policies/meshtrafficpermission/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/listeners"
+)
+
+var _ = Describe("RBACConfigurer", func() {
+
+	type testCase struct {
+		rules    core_xds.Rules
+		mesh     string
+		stats    string
+		expected string
+	}
+
+	DescribeTable("should generate proper envoy config",
+		func(given testCase) {
+			// given
+			configurer := &xds.RBACConfigurer{
+				Rules:     given.rules,
+				Mesh:      given.mesh,
+				StatsName: given.stats,
+			}
+			res, err := listeners.NewFilterChainBuilder(envoy.APIV3).Build()
+			Expect(err).ToNot(HaveOccurred())
+
+			// when
+			err = configurer.Configure(res.(*listenerv3.FilterChain))
+			Expect(err).ToNot(HaveOccurred())
+
+			// then
+			actual, err := util_proto.ToYAML(res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("allow all", testCase{
+			stats: "allow_all_prefix",
+			mesh:  "allow_all_mesh",
+			rules: []*core_xds.Rule{
+				{
+					Subset: []core_xds.Tag{},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "ALLOW",
+					},
+				},
+			},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules:
+          policies:
+              MeshTrafficPermission:
+                  permissions:
+                      - any: true
+                  principals:
+                      - any: true
+      statPrefix: allow_all_prefix.`,
+		}),
+		Entry("deny all", testCase{
+			stats: "deny_all_prefix",
+			mesh:  "deny_all_mesh",
+			rules: []*core_xds.Rule{
+				{
+					Subset: []core_xds.Tag{},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "DENY",
+					},
+				},
+			},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules: {}
+      statPrefix: deny_all_prefix.`,
+		}),
+		Entry("allow backend-v1 and web in us-east", testCase{
+			stats: "allow_2_services_prefix",
+			mesh:  "allow_2_service_mesh",
+			rules: []*core_xds.Rule{
+				{
+					Subset: []core_xds.Tag{
+						{Key: "kuma.io/service", Value: "backend"},
+						{Key: "version", Value: "v1"},
+					},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "ALLOW",
+					},
+				},
+				{
+					Subset: []core_xds.Tag{
+						{Key: "kuma.io/service", Value: "web"},
+						{Key: "kuma.io/zone", Value: "us-east"},
+					},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "ALLOW",
+					},
+				},
+			},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules:
+          policies:
+              MeshTrafficPermission:
+                  permissions:
+                      - any: true
+                  principals:
+                      - andIds:
+                          ids:
+                              - authenticated:
+                                  principalName:
+                                      exact: spiffe://allow_2_service_mesh/backend
+                              - authenticated:
+                                  principalName:
+                                      exact: kuma://version/v1
+                      - andIds:
+                          ids:
+                              - authenticated:
+                                  principalName:
+                                      exact: spiffe://allow_2_service_mesh/web
+                              - authenticated:
+                                  principalName:
+                                      exact: kuma://kuma.io/zone/us-east
+      statPrefix: allow_2_services_prefix.`,
+		}),
+		Entry("allow rule with negation in kuma tag", testCase{
+			stats: "allow_negation_prefix",
+			mesh:  "allow_negation_mesh",
+			rules: []*core_xds.Rule{
+				{
+					Subset: []core_xds.Tag{
+						{Key: "kuma.io/service", Value: "backend"},
+						{Key: "version", Value: "v2", Not: true},
+					},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "ALLOW",
+					},
+				},
+			},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules:
+          policies:
+              MeshTrafficPermission:
+                  permissions:
+                      - any: true
+                  principals:
+                      - andIds:
+                          ids:
+                              - authenticated:
+                                  principalName:
+                                      exact: spiffe://allow_negation_mesh/backend
+                              - notId:
+                                  authenticated:
+                                      principalName:
+                                          exact: kuma://version/v2
+      statPrefix: allow_negation_prefix.`,
+		}),
+		Entry("allow rule with negation in service tag", testCase{
+			stats: "allow_negation_prefix",
+			mesh:  "allow_negation_mesh",
+			rules: []*core_xds.Rule{
+				{
+					Subset: []core_xds.Tag{
+						{Key: "kuma.io/service", Value: "backend", Not: true},
+						{Key: "version", Value: "v2"},
+					},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "ALLOW",
+					},
+				},
+			},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules:
+          policies:
+              MeshTrafficPermission:
+                  permissions:
+                      - any: true
+                  principals:
+                      - andIds:
+                          ids:
+                              - notId:
+                                  authenticated:
+                                      principalName:
+                                          exact: spiffe://allow_negation_mesh/backend
+                              - authenticated:
+                                  principalName:
+                                      exact: kuma://version/v2
+      statPrefix: allow_negation_prefix.`,
+		}),
+		Entry("shadow allow rule", testCase{
+			stats: "shadow_allow_prefix",
+			mesh:  "shadow_allow_mesh",
+			rules: []*core_xds.Rule{
+				{
+					Subset: []core_xds.Tag{
+						{Key: "kuma.io/service", Value: "backend"},
+					},
+					Conf: &v1alpha1.MeshTrafficPermission_Conf{
+						Action: "DENY_WITH_SHADOW_ALLOW",
+					},
+				},
+			},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules: {}
+      shadowRules:
+          policies:
+              MeshTrafficPermission:
+                  permissions:
+                      - any: true
+                  principals:
+                      - authenticated:
+                          principalName:
+                              exact: spiffe://shadow_allow_mesh/backend
+      statPrefix: shadow_allow_prefix.`,
+		}),
+		Entry("no rules", testCase{
+			stats: "no",
+			mesh:  "nothing",
+			rules: []*core_xds.Rule{},
+			expected: `
+filters:
+  - name: envoy.filters.network.rbac
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
+      rules: {}
+      statPrefix: no.`,
+		}),
+	)
+})
diff --git a/pkg/plugins/policies/meshtrafficpermission/xds/suite_test.go b/pkg/plugins/policies/meshtrafficpermission/xds/suite_test.go
new file mode 100644
index 000000000..ea91e81e8
--- /dev/null
+++ b/pkg/plugins/policies/meshtrafficpermission/xds/suite_test.go
@@ -0,0 +1,11 @@
+package xds_test
+
+import (
+	"testing"
+
+	"github.com/kumahq/kuma/pkg/test"
+)
+
+func TestXDS(t *testing.T) {
+	test.RunSpecs(t, "XDS Suite")
+}
diff --git a/test/e2e_env/universal/meshtrafficpermission/meshtrafficpermission.go b/test/e2e_env/universal/meshtrafficpermission/meshtrafficpermission.go
new file mode 100644
index 000000000..bcf76099a
--- /dev/null
+++ b/test/e2e_env/universal/meshtrafficpermission/meshtrafficpermission.go
@@ -0,0 +1,110 @@
+package meshtrafficpermission
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/test/e2e_env/universal/env"
+	. "github.com/kumahq/kuma/test/framework"
+)
+
+func MeshTrafficPermissionUniversal() {
+	meshName := "meshtrafficpermission"
+
+	BeforeAll(func() {
+		Expect(NewClusterSetup().
+			Install(MTLSMeshUniversal(meshName)).
+			Install(TestServerUniversal("test-server", meshName, WithArgs([]string{"echo", "--instance", "echo-v1"}))).
+			Install(DemoClientUniversal(AppModeDemoClient, meshName, WithTransparentProxy(true))).
+			Setup(env.Cluster)).To(Succeed())
+
+		// remove default traffic permission
+		err := env.Cluster.GetKumactlOptions().KumactlDelete("traffic-permission", "allow-all-"+meshName, meshName)
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	E2EAfterAll(func() {
+		Expect(env.Cluster.DeleteMeshApps(meshName)).To(Succeed())
+		Expect(env.Cluster.DeleteMesh(meshName)).To(Succeed())
+	})
+
+	E2EAfterEach(func() {
+		// remove all MeshTrafficPermissions
+		items, err := env.Cluster.GetKumactlOptions().KumactlList("meshtrafficpermissions", meshName)
+		Expect(err).ToNot(HaveOccurred())
+		for _, item := range items {
+			err := env.Cluster.GetKumactlOptions().KumactlDelete("meshtrafficpermission", item, meshName)
+			Expect(err).ToNot(HaveOccurred())
+		}
+	})
+
+	trafficAllowed := func() {
+		stdout, _, err := env.Cluster.ExecWithRetries("", "", "demo-client",
+			"curl", "-v", "--fail", "test-server.mesh")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(stdout).To(ContainSubstring("HTTP/1.1 200 OK"))
+	}
+
+	trafficBlocked := func() {
+		Eventually(func() error {
+			_, _, err := env.Cluster.Exec("", "", "demo-client",
+				"curl", "-v", "--fail", "test-server.mesh")
+			return err
+		}, "30s", "1s").Should(HaveOccurred())
+	}
+
+	It("should allow the traffic with meshtrafficpermission based on MeshService", func() {
+		// given no mesh traffic permissions
+		trafficBlocked()
+
+		// when mesh traffic permission with MeshService
+		yaml := `
+type: MeshTrafficPermission
+name: mtp-1
+mesh: meshtrafficpermission
+spec:
+ targetRef:
+   kind: MeshService
+   name: test-server
+ from:
+   - targetRef:
+       kind: MeshService
+       name: demo-client
+     default:
+       action: ALLOW
+`
+		err := YamlUniversal(yaml)(env.Cluster)
+		Expect(err).ToNot(HaveOccurred())
+
+		// then
+		trafficAllowed()
+	})
+
+	It("should allow the traffic with traffic permission based on non standard tag", func() {
+		// given no mesh traffic permission
+		trafficBlocked()
+
+		// when
+		yaml := `
+type: MeshTrafficPermission
+name: mtp-2
+mesh: meshtrafficpermission
+spec:
+  targetRef:
+    kind: MeshSubset
+    tags:
+      team: server-owners
+  from:
+    - targetRef:
+        kind: MeshSubset
+        tags: 
+          team: client-owners
+      default:
+        action: ALLOW
+`
+		err := YamlUniversal(yaml)(env.Cluster)
+		Expect(err).ToNot(HaveOccurred())
+
+		trafficAllowed()
+	})
+}
diff --git a/test/e2e_env/universal/universal_suite_test.go b/test/e2e_env/universal/universal_suite_test.go
index 6fac0ae8f..0411051d1 100644
--- a/test/e2e_env/universal/universal_suite_test.go
+++ b/test/e2e_env/universal/universal_suite_test.go
@@ -19,6 +19,7 @@ import (
 	"github.com/kumahq/kuma/test/e2e_env/universal/inspect"
 	"github.com/kumahq/kuma/test/e2e_env/universal/matching"
 	"github.com/kumahq/kuma/test/e2e_env/universal/membership"
+	"github.com/kumahq/kuma/test/e2e_env/universal/meshtrafficpermission"
 	"github.com/kumahq/kuma/test/e2e_env/universal/mtls"
 	"github.com/kumahq/kuma/test/e2e_env/universal/observability"
 	"github.com/kumahq/kuma/test/e2e_env/universal/proxytemplate"
@@ -109,3 +110,4 @@ var _ = Describe("Traffic Route", trafficroute.TrafficRoute, Ordered)
 var _ = Describe("Zone Egress", zoneegress.ExternalServices, Ordered)
 var _ = Describe("Virtual Outbound", virtualoutbound.VirtualOutbound, Ordered)
 var _ = Describe("Transparent Proxy", transparentproxy.TransparentProxy, Ordered)
+var _ = Describe("Mesh Traffic Permission", meshtrafficpermission.MeshTrafficPermissionUniversal, Ordered)