Repository: kumahq/kuma
Pull request #1405 (2021-01-11 15:21:20)

feat(kuma-cp) proxy template V3 (bp #1398)
This is an automated backport of pull request #1398 done by Mergify

Cherry-pick of 5eaee1904f1b59ba6057cb2ed859ebe94e9bf74f has failed:
```
On branch mergify/bp/release-1.0/pr-1398
Your branch is up to date with 'origin/release-1.0'.

You are currently cherry-picking commit 5eaee1904.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	modified:   pkg/core/xds/resource.go
	modified:   pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
	modified:   pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
	modified:   pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
	modified:   pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
	modified:   pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
	modified:   pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
	modified:   pkg/xds/generator/modifications/modifications.go
	renamed:    pkg/xds/generator/modifications/cluster.go -> pkg/xds/generator/modifications/v2/cluster.go
	renamed:    pkg/xds/generator/modifications/cluster_test.go -> pkg/xds/generator/modifications/v2/cluster_test.go
	renamed:    pkg/xds/generator/modifications/http_filter.go -> pkg/xds/generator/modifications/v2/http_filter.go
	renamed:    pkg/xds/generator/modifications/http_filter_test.go -> pkg/xds/generator/modifications/v2/http_filter_test.go
	renamed:    pkg/xds/generator/modifications/listener.go -> pkg/xds/generator/modifications/v2/listener.go
	renamed:    pkg/xds/generator/modifications/listener_test.go -> pkg/xds/generator/modifications/v2/listener_test.go
	new file:   pkg/xds/generator/modifications/v2/modifications.go
	renamed:    pkg/xds/generator/modifications/modifications_suite_test.go -> pkg/xds/generator/modifications/v2/modifications_suite_test.go
	renamed:    pkg/xds/generator/modifications/network_filter.go -> pkg/xds/generator/modifications/v2/network_filter.go
	renamed:    pkg/xds/generator/modifications/network_filter_test.go -> pkg/xds/generator/modifications/v2/network_filter_test.go
	renamed:    pkg/xds/generator/modifications/virtual_host.go -> pkg/xds/generator/modifications/v2/virtual_host.go
	renamed:    pkg/xds/generator/modifications/virtual_host_test.go -> pkg/xds/generator/modifications/v2/virtual_host_test.go
	new file:   pkg/xds/generator/modifications/v3/cluster.go
	new file:   pkg/xds/generator/modifications/v3/cluster_test.go
	new file:   pkg/xds/generator/modifications/v3/http_filter.go
	new file:   pkg/xds/generator/modifications/v3/http_filter_test.go
	new file:   pkg/xds/generator/modifications/v3/listener.go
	new file:   pkg/xds/generator/modifications/v3/listener_test.go
	new file:   pkg/xds/generator/modifications/v3/modifications.go
	new file:   pkg/xds/generator/modifications/v3/modifications_suite_test.go
	new file:   pkg/xds/generator/modifications/v3/network_filter.go
	new file:   pkg/xds/generator/modifications/v3/network_filter_test.go
	new file:   pkg/xds/generator/modifications/v3/virtual_host.go
	new file:   pkg/xds/generator/modifications/v3/virtual_host_test.go
	modified:   pkg/xds/generator/proxy_template.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   pkg/xds/envoy/clusters/v3/health_check_configurer_test.go

```

To fixup this pull request, you can check out it locally. See documentation: https://help.github.com/articles/checking-out-pull-requests-locally/
---


<details>
<summary>Mergify commands and options</summary>

<br />

More conditions and actions can be found in the [documentation](https://docs.mergify.io/).

You can also trigger Mergify actions by commenting on this pull request:

- `@Mergifyio refresh` will re-evaluate the rules
- `@Mergifyio rebase` will rebase this PR on its base branch
- `@Mergifyio update` will merge the base branch into this PR
- `@Mergifyio backport <destination>` will backport this PR on `<destination>` branch

Additionally, on Mergify [dashboard](https://dashboard.mergify.io/) you can:

- look at your merge queues
- generate the Mergify configuration with the config editor.

Finally, you can contact us on https://mergify.io/
</details>



Patch:
diff --git a/pkg/core/xds/resource.go b/pkg/core/xds/resource.go
index c7b88ced8..7a8c673ac 100644
--- a/pkg/core/xds/resource.go
+++ b/pkg/core/xds/resource.go
@@ -5,7 +5,8 @@ import (
 
 	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
-	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v2"
+	envoy_resource_v2 "github.com/envoyproxy/go-control-plane/pkg/resource/v2"
+	envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/ptypes"
 )
@@ -146,10 +147,15 @@ func (s *ResourceSet) List() ResourceList {
 		return nil
 	}
 	list := ResourceList{}
-	list = append(list, s.ListOf(envoy_resource.EndpointType)...)
-	list = append(list, s.ListOf(envoy_resource.ClusterType)...)
-	list = append(list, s.ListOf(envoy_resource.RouteType)...)
-	list = append(list, s.ListOf(envoy_resource.ListenerType)...)
-	list = append(list, s.ListOf(envoy_resource.SecretType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.EndpointType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.EndpointType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.ClusterType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.ClusterType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.RouteType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.RouteType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.ListenerType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.ListenerType)...)
+	list = append(list, s.ListOf(envoy_resource_v2.SecretType)...)
+	list = append(list, s.ListOf(envoy_resource_v3.SecretType)...)
 	return list
 }
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
index b5019132c..a7e5a1fab 100644
--- a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
@@ -16,6 +16,7 @@ func (e *EdsClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
 	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_EDS}
 	c.EdsClusterConfig = &envoy_cluster.Cluster_EdsClusterConfig{
 		EdsConfig: &envoy_core.ConfigSource{
+			ResourceApiVersion: envoy_core.ApiVersion_V3,
 			ConfigSourceSpecifier: &envoy_core.ConfigSource_Ads{
 				Ads: &envoy_core.AggregatedConfigSource{},
 			},
diff --git a/pkg/xds/generator/modifications/modifications.go b/pkg/xds/generator/modifications/modifications.go
index c9eead727..9f5f0a665 100644
--- a/pkg/xds/generator/modifications/modifications.go
+++ b/pkg/xds/generator/modifications/modifications.go
@@ -5,40 +5,18 @@ import (
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	modifications_v2 "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
+	modifications_v3 "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
 )
 
-// OriginProxyTemplateModifications is a marker to indicate by which ProxyGenerator resources were generated.
-const OriginProxyTemplateModifications = "proxy-template-modifications"
-
-type modificator interface {
-	apply(*core_xds.ResourceSet) error
-}
-
-func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications) error {
-	for i, modification := range modifications {
-		var modificator modificator
-		switch modification.Type.(type) {
-		case *mesh_proto.ProxyTemplate_Modifications_Cluster_:
-			mod := clusterModificator(*modification.GetCluster())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_Listener_:
-			mod := listenerModificator(*modification.GetListener())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_NetworkFilter_:
-			mod := networkFilterModificator(*modification.GetNetworkFilter())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
-			mod := httpFilterModificator(*modification.GetHttpFilter())
-			modificator = &mod
-		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
-			mod := virtualHostModificator(*modification.GetVirtualHost())
-			modificator = &mod
-		default:
-			return errors.Errorf("invalid modification type %T", modification.Type)
-		}
-		if err := modificator.apply(resources); err != nil {
-			return errors.Wrapf(err, "could not apply %d modification", i)
-		}
+func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications, apiVersion envoy_common.APIVersion) error {
+	switch apiVersion {
+	case envoy_common.APIV2:
+		return modifications_v2.Apply(resources, modifications)
+	case envoy_common.APIV3:
+		return modifications_v3.Apply(resources, modifications)
+	default:
+		return errors.Errorf("unknown API version %s", apiVersion)
 	}
-	return nil
 }
diff --git a/pkg/xds/generator/modifications/cluster.go b/pkg/xds/generator/modifications/v2/cluster.go
similarity index 98%
rename from pkg/xds/generator/modifications/cluster.go
rename to pkg/xds/generator/modifications/v2/cluster.go
index 083ad04f5..8c0cedf63 100644
--- a/pkg/xds/generator/modifications/cluster.go
+++ b/pkg/xds/generator/modifications/v2/cluster.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/http_filter.go b/pkg/xds/generator/modifications/v2/http_filter.go
similarity index 99%
rename from pkg/xds/generator/modifications/http_filter.go
rename to pkg/xds/generator/modifications/v2/http_filter.go
index 7f749e991..b52b8667d 100644
--- a/pkg/xds/generator/modifications/http_filter.go
+++ b/pkg/xds/generator/modifications/v2/http_filter.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/listener.go b/pkg/xds/generator/modifications/v2/listener.go
similarity index 98%
rename from pkg/xds/generator/modifications/listener.go
rename to pkg/xds/generator/modifications/v2/listener.go
index 19c7332b9..fc5616bf0 100644
--- a/pkg/xds/generator/modifications/listener.go
+++ b/pkg/xds/generator/modifications/v2/listener.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/v2/modifications.go b/pkg/xds/generator/modifications/v2/modifications.go
new file mode 100644
index 000000000..5239c77c8
--- /dev/null
+++ b/pkg/xds/generator/modifications/v2/modifications.go
@@ -0,0 +1,44 @@
+package v2
+
+import (
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+)
+
+// OriginProxyTemplateModifications is a marker to indicate by which ProxyGenerator resources were generated.
+const OriginProxyTemplateModifications = "proxy-template-modifications"
+
+type modificator interface {
+	apply(*core_xds.ResourceSet) error
+}
+
+func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications) error {
+	for i, modification := range modifications {
+		var modificator modificator
+		switch modification.Type.(type) {
+		case *mesh_proto.ProxyTemplate_Modifications_Cluster_:
+			mod := clusterModificator(*modification.GetCluster())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_Listener_:
+			mod := listenerModificator(*modification.GetListener())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_NetworkFilter_:
+			mod := networkFilterModificator(*modification.GetNetworkFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
+			mod := httpFilterModificator(*modification.GetHttpFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
+			mod := virtualHostModificator(*modification.GetVirtualHost())
+			modificator = &mod
+		default:
+			return errors.Errorf("invalid modification type %T", modification.Type)
+		}
+		if err := modificator.apply(resources); err != nil {
+			return errors.Wrapf(err, "could not apply %d modification", i)
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/generator/modifications/network_filter.go b/pkg/xds/generator/modifications/v2/network_filter.go
similarity index 99%
rename from pkg/xds/generator/modifications/network_filter.go
rename to pkg/xds/generator/modifications/v2/network_filter.go
index dfe478a6f..0fb3c4e9e 100644
--- a/pkg/xds/generator/modifications/network_filter.go
+++ b/pkg/xds/generator/modifications/v2/network_filter.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/virtual_host.go b/pkg/xds/generator/modifications/v2/virtual_host.go
similarity index 99%
rename from pkg/xds/generator/modifications/virtual_host.go
rename to pkg/xds/generator/modifications/v2/virtual_host.go
index 897d25bac..3fe072334 100644
--- a/pkg/xds/generator/modifications/virtual_host.go
+++ b/pkg/xds/generator/modifications/v2/virtual_host.go
@@ -1,4 +1,4 @@
-package modifications
+package v2
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
diff --git a/pkg/xds/generator/modifications/v3/cluster.go b/pkg/xds/generator/modifications/v3/cluster.go
new file mode 100644
index 000000000..7a23333a3
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/cluster.go
@@ -0,0 +1,66 @@
+package v3
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/proto"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type clusterModificator mesh_proto.ProxyTemplate_Modifications_Cluster
+
+func (c *clusterModificator) apply(resources *core_xds.ResourceSet) error {
+	clusterMod := &envoy_cluster.Cluster{}
+	if err := util_proto.FromYAML([]byte(c.Value), clusterMod); err != nil {
+		return err
+	}
+	switch c.Operation {
+	case mesh_proto.OpAdd:
+		c.add(resources, clusterMod)
+	case mesh_proto.OpRemove:
+		c.remove(resources)
+	case mesh_proto.OpPatch:
+		c.patch(resources, clusterMod)
+	default:
+		return errors.Errorf("invalid operation: %s", c.Operation)
+	}
+	return nil
+}
+
+func (c *clusterModificator) patch(resources *core_xds.ResourceSet, clusterMod *envoy_cluster.Cluster) {
+	for _, cluster := range resources.Resources(envoy_resource.ClusterType) {
+		if c.clusterMatches(cluster) {
+			proto.Merge(cluster.Resource, clusterMod)
+		}
+	}
+}
+
+func (c *clusterModificator) remove(resources *core_xds.ResourceSet) {
+	for name, resource := range resources.Resources(envoy_resource.ClusterType) {
+		if c.clusterMatches(resource) {
+			resources.Remove(envoy_resource.ClusterType, name)
+		}
+	}
+}
+
+func (c *clusterModificator) add(resources *core_xds.ResourceSet, clusterMod *envoy_cluster.Cluster) *core_xds.ResourceSet {
+	return resources.Add(&core_xds.Resource{
+		Name:     clusterMod.Name,
+		Origin:   OriginProxyTemplateModifications,
+		Resource: clusterMod,
+	})
+}
+
+func (c *clusterModificator) clusterMatches(cluster *core_xds.Resource) bool {
+	if c.Match.GetName() != "" && c.Match.GetName() != cluster.Name {
+		return false
+	}
+	if c.Match.GetOrigin() != "" && c.Match.GetOrigin() != cluster.Origin {
+		return false
+	}
+	return true
+}
diff --git a/pkg/xds/generator/modifications/v3/http_filter.go b/pkg/xds/generator/modifications/v3/http_filter.go
new file mode 100644
index 000000000..1f7082fb1
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/http_filter.go
@@ -0,0 +1,147 @@
+package v3
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/ptypes"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type httpFilterModificator mesh_proto.ProxyTemplate_Modifications_HttpFilter
+
+func (h *httpFilterModificator) apply(resources *core_xds.ResourceSet) error {
+	for _, resource := range resources.Resources(envoy_resource.ListenerType) {
+		if h.listenerMatches(resource) {
+			listener := resource.Resource.(*envoy_listener.Listener)
+			for _, chain := range listener.FilterChains { // apply on all filter chains. We could introduce filter chain matcher as an improvement.
+				for _, networkFilter := range chain.Filters {
+					if networkFilter.Name == "envoy.filters.network.http_connection_manager" {
+						hcm := &envoy_hcm.HttpConnectionManager{}
+						err := ptypes.UnmarshalAny(networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig, hcm)
+						if err != nil {
+							return err
+						}
+						if err := h.applyHCMModification(hcm); err != nil {
+							return err
+						}
+						any, err := util_proto.MarshalAnyDeterministic(hcm)
+						if err != nil {
+							return err
+						}
+						networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig = any
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (h *httpFilterModificator) applyHCMModification(hcm *envoy_hcm.HttpConnectionManager) error {
+	filter := &envoy_hcm.HttpFilter{}
+	if err := util_proto.FromYAML([]byte(h.Value), filter); err != nil {
+		return err
+	}
+	switch h.Operation {
+	case mesh_proto.OpAddFirst:
+		h.addFirst(hcm, filter)
+	case mesh_proto.OpAddLast:
+		h.addLast(hcm, filter)
+	case mesh_proto.OpAddAfter:
+		h.addAfter(hcm, filter)
+	case mesh_proto.OpAddBefore:
+		h.addBefore(hcm, filter)
+	case mesh_proto.OpRemove:
+		h.remove(hcm)
+	case mesh_proto.OpPatch:
+		if err := h.patch(hcm, filter); err != nil {
+			return errors.Wrap(err, "could not patch the resource")
+		}
+	default:
+		return errors.Errorf("invalid operation: %s", h.Operation)
+	}
+	return nil
+}
+
+func (h *httpFilterModificator) patch(hcm *envoy_hcm.HttpConnectionManager, filterPatch *envoy_hcm.HttpFilter) error {
+	for _, filter := range hcm.HttpFilters {
+		if h.filterMatches(filter) {
+			any, err := util_proto.MergeAnys(filter.GetTypedConfig(), filterPatch.GetTypedConfig())
+			if err != nil {
+				return err
+			}
+
+			filter.ConfigType = &envoy_hcm.HttpFilter_TypedConfig{
+				TypedConfig: any,
+			}
+		}
+	}
+	return nil
+}
+
+func (h *httpFilterModificator) remove(hcm *envoy_hcm.HttpConnectionManager) {
+	var filters []*envoy_hcm.HttpFilter
+	for _, filter := range hcm.HttpFilters {
+		if !h.filterMatches(filter) {
+			filters = append(filters, filter)
+		}
+	}
+	hcm.HttpFilters = filters
+}
+
+func (h *httpFilterModificator) addBefore(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	idx := h.indexOfMatchedFilter(hcm)
+	if idx != -1 {
+		hcm.HttpFilters = append(hcm.HttpFilters, nil)
+		copy(hcm.HttpFilters[idx+1:], hcm.HttpFilters[idx:])
+		hcm.HttpFilters[idx] = filterMod
+	}
+}
+
+func (h *httpFilterModificator) addAfter(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	idx := h.indexOfMatchedFilter(hcm)
+	if idx != -1 {
+		hcm.HttpFilters = append(hcm.HttpFilters, nil)
+		copy(hcm.HttpFilters[idx+2:], hcm.HttpFilters[idx+1:])
+		hcm.HttpFilters[idx+1] = filterMod
+	}
+}
+
+func (h *httpFilterModificator) addLast(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	hcm.HttpFilters = append(hcm.HttpFilters, filterMod)
+}
+
+func (h *httpFilterModificator) addFirst(hcm *envoy_hcm.HttpConnectionManager, filterMod *envoy_hcm.HttpFilter) {
+	hcm.HttpFilters = append([]*envoy_hcm.HttpFilter{filterMod}, hcm.HttpFilters...)
+}
+
+func (h *httpFilterModificator) filterMatches(filter *envoy_hcm.HttpFilter) bool {
+	if h.Match.GetName() != "" && h.Match.GetName() != filter.Name {
+		return false
+	}
+	return true
+}
+
+func (h *httpFilterModificator) listenerMatches(resource *core_xds.Resource) bool {
+	if h.Match.GetListenerName() != "" && h.Match.GetListenerName() != resource.Name {
+		return false
+	}
+	if h.Match.GetOrigin() != "" && h.Match.GetOrigin() != resource.Origin {
+		return false
+	}
+	return true
+}
+
+func (h *httpFilterModificator) indexOfMatchedFilter(hcm *envoy_hcm.HttpConnectionManager) int {
+	for i, filter := range hcm.HttpFilters {
+		if filter.Name == h.Match.Name {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/pkg/xds/generator/modifications/v3/listener.go b/pkg/xds/generator/modifications/v3/listener.go
new file mode 100644
index 000000000..6df0cf586
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/listener.go
@@ -0,0 +1,66 @@
+package v3
+
+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/golang/protobuf/proto"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type listenerModificator mesh_proto.ProxyTemplate_Modifications_Listener
+
+func (l *listenerModificator) apply(resources *core_xds.ResourceSet) error {
+	listener := &envoy_listener.Listener{}
+	if err := util_proto.FromYAML([]byte(l.Value), listener); err != nil {
+		return err
+	}
+	switch l.Operation {
+	case mesh_proto.OpAdd:
+		l.app(resources, listener)
+	case mesh_proto.OpRemove:
+		l.remove(resources)
+	case mesh_proto.OpPatch:
+		l.patch(resources, listener)
+	default:
+		return errors.Errorf("invalid operation: %s", l.Operation)
+	}
+	return nil
+}
+
+func (l *listenerModificator) patch(resources *core_xds.ResourceSet, listenerPatch *envoy_listener.Listener) {
+	for _, listener := range resources.Resources(envoy_resource.ListenerType) {
+		if l.listenerMatches(listener) {
+			proto.Merge(listener.Resource, listenerPatch)
+		}
+	}
+}
+
+func (l *listenerModificator) remove(resources *core_xds.ResourceSet) {
+	for name, resource := range resources.Resources(envoy_resource.ListenerType) {
+		if l.listenerMatches(resource) {
+			resources.Remove(envoy_resource.ListenerType, name)
+		}
+	}
+}
+
+func (l *listenerModificator) app(resources *core_xds.ResourceSet, listener *envoy_listener.Listener) *core_xds.ResourceSet {
+	return resources.Add(&core_xds.Resource{
+		Name:     listener.Name,
+		Origin:   OriginProxyTemplateModifications,
+		Resource: listener,
+	})
+}
+
+func (l *listenerModificator) listenerMatches(listener *core_xds.Resource) bool {
+	if l.Match.GetName() != "" && l.Match.GetName() != listener.Name {
+		return false
+	}
+	if l.Match.GetOrigin() != "" && l.Match.GetOrigin() != listener.Origin {
+		return false
+	}
+	return true
+}
diff --git a/pkg/xds/generator/modifications/v3/modifications.go b/pkg/xds/generator/modifications/v3/modifications.go
new file mode 100644
index 000000000..e79c37fb5
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/modifications.go
@@ -0,0 +1,44 @@
+package v3
+
+import (
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+)
+
+// OriginProxyTemplateModifications is a marker to indicate by which ProxyGenerator resources were generated.
+const OriginProxyTemplateModifications = "proxy-template-modifications"
+
+type modificator interface {
+	apply(*core_xds.ResourceSet) error
+}
+
+func Apply(resources *core_xds.ResourceSet, modifications []*mesh_proto.ProxyTemplate_Modifications) error {
+	for i, modification := range modifications {
+		var modificator modificator
+		switch modification.Type.(type) {
+		case *mesh_proto.ProxyTemplate_Modifications_Cluster_:
+			mod := clusterModificator(*modification.GetCluster())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_Listener_:
+			mod := listenerModificator(*modification.GetListener())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_NetworkFilter_:
+			mod := networkFilterModificator(*modification.GetNetworkFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_HttpFilter_:
+			mod := httpFilterModificator(*modification.GetHttpFilter())
+			modificator = &mod
+		case *mesh_proto.ProxyTemplate_Modifications_VirtualHost_:
+			mod := virtualHostModificator(*modification.GetVirtualHost())
+			modificator = &mod
+		default:
+			return errors.Errorf("invalid modification type %T", modification.Type)
+		}
+		if err := modificator.apply(resources); err != nil {
+			return errors.Wrapf(err, "could not apply %d modification", i)
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/generator/modifications/v3/network_filter.go b/pkg/xds/generator/modifications/v3/network_filter.go
new file mode 100644
index 000000000..ef0e104b8
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/network_filter.go
@@ -0,0 +1,124 @@
+package v3
+
+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/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type networkFilterModificator mesh_proto.ProxyTemplate_Modifications_NetworkFilter
+
+func (n *networkFilterModificator) apply(resources *core_xds.ResourceSet) error {
+	filter := &envoy_listener.Filter{}
+	if err := util_proto.FromYAML([]byte(n.Value), filter); err != nil {
+		return err
+	}
+	for _, resource := range resources.Resources(envoy_resource.ListenerType) {
+		if n.listenerMatches(resource) {
+			listener := resource.Resource.(*envoy_listener.Listener)
+			for _, chain := range listener.FilterChains { // apply on all filter chains. We could introduce filter chain matcher as an improvement.
+				switch n.Operation {
+				case mesh_proto.OpAddFirst:
+					n.addFirst(chain, filter)
+				case mesh_proto.OpAddLast:
+					n.addLast(chain, filter)
+				case mesh_proto.OpAddAfter:
+					n.addAfter(chain, filter)
+				case mesh_proto.OpAddBefore:
+					n.addBefore(chain, filter)
+				case mesh_proto.OpRemove:
+					n.remove(chain)
+				case mesh_proto.OpPatch:
+					if err := n.patch(chain, filter); err != nil {
+						return errors.Wrap(err, "could not patch the resource")
+					}
+				default:
+					return errors.Errorf("invalid operation: %s", n.Operation)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (n *networkFilterModificator) addFirst(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	chain.Filters = append([]*envoy_listener.Filter{filter}, chain.Filters...)
+}
+
+func (n *networkFilterModificator) addLast(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	chain.Filters = append(chain.Filters, filter)
+}
+
+func (n *networkFilterModificator) addAfter(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	idx := n.indexOfMatchedFilter(chain)
+	if idx != -1 {
+		chain.Filters = append(chain.Filters, nil)
+		copy(chain.Filters[idx+2:], chain.Filters[idx+1:])
+		chain.Filters[idx+1] = filter
+	}
+}
+
+func (n *networkFilterModificator) addBefore(chain *envoy_listener.FilterChain, filter *envoy_listener.Filter) {
+	idx := n.indexOfMatchedFilter(chain)
+	if idx != -1 {
+		chain.Filters = append(chain.Filters, nil)
+		copy(chain.Filters[idx+1:], chain.Filters[idx:])
+		chain.Filters[idx] = filter
+	}
+}
+
+func (n *networkFilterModificator) remove(chain *envoy_listener.FilterChain) {
+	var filters []*envoy_listener.Filter
+	for _, filter := range chain.Filters {
+		if !n.filterMatches(filter) {
+			filters = append(filters, filter)
+		}
+	}
+	chain.Filters = filters
+}
+
+func (n *networkFilterModificator) patch(chain *envoy_listener.FilterChain, filterPatch *envoy_listener.Filter) error {
+	for _, filter := range chain.Filters {
+		if n.filterMatches(filter) {
+			any, err := util_proto.MergeAnys(filter.GetTypedConfig(), filterPatch.GetTypedConfig())
+			if err != nil {
+				return err
+			}
+
+			filter.ConfigType = &envoy_listener.Filter_TypedConfig{
+				TypedConfig: any,
+			}
+		}
+	}
+	return nil
+}
+
+func (n *networkFilterModificator) filterMatches(filter *envoy_listener.Filter) bool {
+	if n.Match.GetName() != "" && n.Match.GetName() != filter.Name {
+		return false
+	}
+	return true
+}
+
+func (n *networkFilterModificator) listenerMatches(resource *core_xds.Resource) bool {
+	if n.Match.GetListenerName() != "" && n.Match.GetListenerName() != resource.Name {
+		return false
+	}
+	if n.Match.GetOrigin() != "" && n.Match.GetOrigin() != resource.Origin {
+		return false
+	}
+	return true
+}
+
+func (n *networkFilterModificator) indexOfMatchedFilter(chain *envoy_listener.FilterChain) int {
+	for i, filter := range chain.Filters {
+		if filter.Name == n.Match.Name {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/pkg/xds/generator/modifications/v3/virtual_host.go b/pkg/xds/generator/modifications/v3/virtual_host.go
new file mode 100644
index 000000000..53c4aa943
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/virtual_host.go
@@ -0,0 +1,114 @@
+package v3
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+	envoy_route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
+	envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
+	envoy_resource "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/ptypes"
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+// virtualHostModificator assumes that the routes are specified as `routeConfig` in Listeners, not through RDS
+// If we ever change it to RDS we need to modify RouteConfiguration objects
+type virtualHostModificator mesh_proto.ProxyTemplate_Modifications_VirtualHost
+
+func (c *virtualHostModificator) apply(resources *core_xds.ResourceSet) error {
+	virtualHost := &envoy_route.VirtualHost{}
+	if err := util_proto.FromYAML([]byte(c.Value), virtualHost); err != nil {
+		return err
+	}
+
+	for _, resource := range resources.Resources(envoy_resource.ListenerType) {
+		listener := resource.Resource.(*envoy_listener.Listener)
+		if !c.originMatches(resource) {
+			continue
+		}
+		for _, chain := range listener.FilterChains { // apply on all filter chains. We could introduce filter chain matcher as an improvement.
+			for _, networkFilter := range chain.Filters {
+				if networkFilter.Name == "envoy.filters.network.http_connection_manager" {
+					hcm := &envoy_hcm.HttpConnectionManager{}
+					err := ptypes.UnmarshalAny(networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig, hcm)
+					if err != nil {
+						return err
+					}
+					if err := c.applyHCMModification(hcm, virtualHost); err != nil {
+						return err
+					}
+					any, err := util_proto.MarshalAnyDeterministic(hcm)
+					if err != nil {
+						return err
+					}
+					networkFilter.ConfigType.(*envoy_listener.Filter_TypedConfig).TypedConfig = any
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (c *virtualHostModificator) applyHCMModification(hcm *envoy_hcm.HttpConnectionManager, virtualHost *envoy_route.VirtualHost) error {
+	routeCfg := hcm.GetRouteConfig()
+	if routeCfg == nil {
+		return nil // ignore HCMs without embedded routes
+	}
+	if !c.routeConfigurationMatches(routeCfg) {
+		return nil
+	}
+	switch c.Operation {
+	case mesh_proto.OpAdd:
+		c.add(routeCfg, virtualHost)
+	case mesh_proto.OpRemove:
+		c.remove(routeCfg)
+	case mesh_proto.OpPatch:
+		c.patch(routeCfg, virtualHost)
+	default:
+		return errors.Errorf("invalid operation: %s", c.Operation)
+	}
+	return nil
+}
+
+func (c *virtualHostModificator) patch(routeCfg *envoy_route.RouteConfiguration, vHostPatch *envoy_route.VirtualHost) {
+	for _, vHost := range routeCfg.VirtualHosts {
+		if c.virtualHostMatches(vHost) {
+			proto.Merge(vHost, vHostPatch)
+		}
+	}
+}
+
+func (c *virtualHostModificator) remove(routeCfg *envoy_route.RouteConfiguration) {
+	var vHosts []*envoy_route.VirtualHost
+	for _, vHost := range routeCfg.VirtualHosts {
+		if !c.virtualHostMatches(vHost) {
+			vHosts = append(vHosts, vHost)
+		}
+	}
+	routeCfg.VirtualHosts = vHosts
+}
+
+func (c *virtualHostModificator) add(routeCfg *envoy_route.RouteConfiguration, vHost *envoy_route.VirtualHost) {
+	routeCfg.VirtualHosts = append(routeCfg.VirtualHosts, vHost)
+}
+
+func (c *virtualHostModificator) virtualHostMatches(vHost *envoy_route.VirtualHost) bool {
+	if c.Match.GetName() != "" && c.Match.GetName() != vHost.Name {
+		return false
+	}
+	return true
+}
+
+func (c *virtualHostModificator) originMatches(routeCfg *core_xds.Resource) bool {
+	return c.Match.GetOrigin() == "" || (c.Match.GetOrigin() == routeCfg.Origin)
+}
+
+func (c *virtualHostModificator) routeConfigurationMatches(routeCfg *envoy_route.RouteConfiguration) bool {
+	if c.Match.GetRouteConfigurationName() != "" && c.Match.GetRouteConfigurationName() != routeCfg.Name {
+		return false
+	}
+	return true
+}
diff --git a/pkg/xds/generator/proxy_template.go b/pkg/xds/generator/proxy_template.go
index c921b3143..9e03b91e8 100644
--- a/pkg/xds/generator/proxy_template.go
+++ b/pkg/xds/generator/proxy_template.go
@@ -34,7 +34,7 @@ func (g *ProxyTemplateGenerator) Generate(ctx xds_context.Context, proxy *model.
 	} else {
 		resources.AddSet(rs)
 	}
-	if err := modifications.Apply(resources, g.ProxyTemplate.GetConf().GetModifications()); err != nil {
+	if err := modifications.Apply(resources, g.ProxyTemplate.GetConf().GetModifications(), proxy.APIVersion); err != nil {
 		return nil, errors.Wrap(err, "could not apply modifications")
 	}
 	return resources, nil


Test patch:
diff --git a/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
index de9323588..6c173bc0c 100644
--- a/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
@@ -51,6 +51,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 100
               enforcingConsecutiveGatewayFailure: 0
@@ -74,6 +75,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 100
@@ -97,6 +99,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -122,6 +125,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               consecutive5xx: 21
               consecutiveGatewayFailure: 11
@@ -148,6 +152,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -176,6 +181,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -203,6 +209,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
@@ -231,6 +238,7 @@ var _ = Describe("OutlierDetectionConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             outlierDetection:
               enforcingConsecutive5xx: 0
               enforcingConsecutiveGatewayFailure: 0
diff --git a/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
index e7dc1c563..28068854c 100644
--- a/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
@@ -76,6 +76,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             transportSocket:
               name: envoy.transport_sockets.tls
@@ -158,6 +159,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             transportSocketMatches:
             - match:
@@ -283,6 +285,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             transportSocket:
               name: envoy.transport_sockets.tls
diff --git a/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
index 0380aa1f6..3d45caffe 100644
--- a/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
@@ -54,6 +54,7 @@ var _ = Describe("ClientSideTLSConfigurer", func() {
         edsClusterConfig:
           edsConfig:
             ads: {}
+            resourceApiVersion: V3
         name: testCluster
         transportSocketMatches:
         - match: {}
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
index 20a2e7c6f..3cb3ae94b 100644
--- a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
@@ -20,6 +20,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
         edsClusterConfig:
           edsConfig:
             ads: {}
+            resourceApiVersion: V3
         name: test:cluster
         type: EDS`
 
diff --git a/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
index 449a6b578..cdc740f55 100644
--- a/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
@@ -47,6 +47,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: testCluster
             type: EDS`,
 		}),
@@ -73,6 +74,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             healthChecks:
             - healthyThreshold: 2
               interval: 5s
diff --git a/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
index 08aaa8b69..47e3b9982 100644
--- a/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
@@ -41,6 +41,7 @@ var _ = Describe("LbSubset", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             name: backend
             type: EDS`,
 		}),
@@ -55,6 +56,7 @@ var _ = Describe("LbSubset", func() {
             edsClusterConfig:
               edsConfig:
                 ads: {}
+                resourceApiVersion: V3
             lbSubsetConfig:
               fallbackPolicy: ANY_ENDPOINT
               subsetSelectors:
diff --git a/pkg/xds/generator/modifications/cluster_test.go b/pkg/xds/generator/modifications/v2/cluster_test.go
similarity index 98%
rename from pkg/xds/generator/modifications/cluster_test.go
rename to pkg/xds/generator/modifications/v2/cluster_test.go
index 8a20cd69f..f721bcf3b 100644
--- a/pkg/xds/generator/modifications/cluster_test.go
+++ b/pkg/xds/generator/modifications/v2/cluster_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -7,7 +7,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
diff --git a/pkg/xds/generator/modifications/http_filter_test.go b/pkg/xds/generator/modifications/v2/http_filter_test.go
similarity index 99%
rename from pkg/xds/generator/modifications/http_filter_test.go
rename to pkg/xds/generator/modifications/v2/http_filter_test.go
index 066a9f77a..a84e49476 100644
--- a/pkg/xds/generator/modifications/http_filter_test.go
+++ b/pkg/xds/generator/modifications/v2/http_filter_test.go
@@ -1,11 +1,11 @@
-package modifications_test
+package v2_test
 
 import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 
diff --git a/pkg/xds/generator/modifications/listener_test.go b/pkg/xds/generator/modifications/v2/listener_test.go
similarity index 98%
rename from pkg/xds/generator/modifications/listener_test.go
rename to pkg/xds/generator/modifications/v2/listener_test.go
index 95666652d..7014c1330 100644
--- a/pkg/xds/generator/modifications/listener_test.go
+++ b/pkg/xds/generator/modifications/v2/listener_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -7,7 +7,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
diff --git a/pkg/xds/generator/modifications/modifications_suite_test.go b/pkg/xds/generator/modifications/v2/modifications_suite_test.go
similarity index 86%
rename from pkg/xds/generator/modifications/modifications_suite_test.go
rename to pkg/xds/generator/modifications/v2/modifications_suite_test.go
index 9db07ae4d..169cbc554 100644
--- a/pkg/xds/generator/modifications/modifications_suite_test.go
+++ b/pkg/xds/generator/modifications/v2/modifications_suite_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	"testing"
@@ -14,6 +14,6 @@ func TestModifications(t *testing.T) {
 	RegisterFailHandler(Fail)
 
 	RunSpecsWithDefaultAndCustomReporters(t,
-		"Modifications Suite",
+		"Modifications V2 Suite",
 		[]Reporter{util_test.NewlineReporter{}})
 }
diff --git a/pkg/xds/generator/modifications/network_filter_test.go b/pkg/xds/generator/modifications/v2/network_filter_test.go
similarity index 99%
rename from pkg/xds/generator/modifications/network_filter_test.go
rename to pkg/xds/generator/modifications/v2/network_filter_test.go
index 1bf94fc60..257000372 100644
--- a/pkg/xds/generator/modifications/network_filter_test.go
+++ b/pkg/xds/generator/modifications/v2/network_filter_test.go
@@ -1,11 +1,11 @@
-package modifications_test
+package v2_test
 
 import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 
diff --git a/pkg/xds/generator/modifications/virtual_host_test.go b/pkg/xds/generator/modifications/v2/virtual_host_test.go
similarity index 99%
rename from pkg/xds/generator/modifications/virtual_host_test.go
rename to pkg/xds/generator/modifications/v2/virtual_host_test.go
index be78a7dd5..411ba821b 100644
--- a/pkg/xds/generator/modifications/virtual_host_test.go
+++ b/pkg/xds/generator/modifications/v2/virtual_host_test.go
@@ -1,4 +1,4 @@
-package modifications_test
+package v2_test
 
 import (
 	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
@@ -7,7 +7,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/generator"
-	"github.com/kumahq/kuma/pkg/xds/generator/modifications"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
diff --git a/pkg/xds/generator/modifications/v3/cluster_test.go b/pkg/xds/generator/modifications/v3/cluster_test.go
new file mode 100644
index 000000000..f05b7c69c
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/cluster_test.go
@@ -0,0 +1,218 @@
+package v3_test
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Cluster modifications", func() {
+
+	type testCase struct {
+		clusters      []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, clusterYAML := range given.clusters {
+				cluster := &envoy_cluster.Cluster{}
+				err := util_proto.FromYAML([]byte(clusterYAML), cluster)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     cluster.Name,
+					Origin:   generator.OriginInbound,
+					Resource: cluster,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add cluster", testCase{
+			modifications: []string{`
+                cluster:
+                   operation: add
+                   value: |
+                     edsClusterConfig:
+                       edsConfig:
+                         ads: {}
+                     name: test:cluster
+                     type: EDS`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                edsClusterConfig:
+                  edsConfig:
+                    ads: {}
+                name: test:cluster
+                type: EDS`,
+		}),
+		Entry("should replace cluster", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: add
+                   value: |
+                     edsClusterConfig:
+                       edsConfig:
+                         ads: {}
+                     name: test:cluster
+                     type: EDS`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                edsClusterConfig:
+                  edsConfig:
+                    ads: {}
+                name: test:cluster
+                type: EDS`,
+		}),
+		Entry("should remove cluster matching all", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: remove`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should remove cluster matching name", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster2
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: remove
+                   match:
+                     name: test:cluster`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster2
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster2
+                type: ORIGINAL_DST`,
+		}),
+		Entry("should remove all inbound clusters", testCase{
+			clusters: []string{
+				`
+                connectTimeout: 5s
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: remove
+                   match:
+                     origin: inbound`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should patch cluster matching name", testCase{
+			clusters: []string{
+				`
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                outlierDetection:
+                  enforcingConsecutive5xx: 100
+                  enforcingConsecutiveGatewayFailure: 0
+                  enforcingConsecutiveLocalOriginFailure: 0
+                  enforcingFailurePercentage: 0
+                  enforcingSuccessRate: 0
+                type: ORIGINAL_DST`,
+			},
+			modifications: []string{
+				`
+                cluster:
+                   operation: patch
+                   match:
+                     name: test:cluster
+                   value: |
+                     connectTimeout: 5s
+                     httpProtocolOptions:
+                       acceptHttp10: true
+                     outlierDetection:
+                       enforcingSuccessRate: 100`,
+			},
+			expected: `
+            resources:
+            - name: test:cluster
+              resource:
+                '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
+                connectTimeout: 5s
+                httpProtocolOptions:
+                  acceptHttp10: true
+                lbPolicy: CLUSTER_PROVIDED
+                name: test:cluster
+                outlierDetection:
+                  enforcingConsecutive5xx: 100
+                  enforcingConsecutiveGatewayFailure: 0
+                  enforcingConsecutiveLocalOriginFailure: 0
+                  enforcingFailurePercentage: 0
+                  enforcingSuccessRate: 100
+                type: ORIGINAL_DST`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/http_filter_test.go b/pkg/xds/generator/modifications/v3/http_filter_test.go
new file mode 100644
index 000000000..57b54b360
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/http_filter_test.go
@@ -0,0 +1,554 @@
+package v3_test
+
+import (
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("HTTP Filter modifications", func() {
+
+	type testCase struct {
+		listeners     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, listenerYAML := range given.listeners {
+				listener := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(listenerYAML), listener)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     listener.Name,
+					Origin:   generator.OriginInbound,
+					Resource: listener,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add filter as a first", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                      httpFilters:
+                      - name: envoy.filters.http.router`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addLast
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters from all listeners when there is no match section", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters from all listeners when there is inbound match section", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+                   match:
+                     origin: inbound
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters from picked listener", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+                   match:
+                     listenerName: inbound:192.168.0.1:8080
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all filters of given name from all listeners", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                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
+                      - name: envoy.filters.http.cors
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: remove
+                   match:
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                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
+                      statPrefix: localhost_8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should add filter after already defined", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.http.router
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should not add filter when name is not matched", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.http.gzip
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should add filter before already defined", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: addBefore
+                   match:
+                     name: envoy.filters.http.gzip
+                   value: |
+                     name: envoy.filters.http.cors
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      - name: envoy.filters.http.cors
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should patch resource matching filter name", testCase{
+			listeners: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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.config.filter.http.router.v2.Router
+                          startChildSpan: true
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+			},
+			modifications: []string{`
+                httpFilter:
+                   operation: patch
+                   match:
+                     name: envoy.filters.http.router
+                   value: |
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.http.router.v2.Router
+                       dynamicStats: false
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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.config.filter.http.router.v2.Router
+                          startChildSpan: true
+                          dynamicStats: false
+                      - name: envoy.filters.http.gzip
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/listener_test.go b/pkg/xds/generator/modifications/v3/listener_test.go
new file mode 100644
index 000000000..4265fd6e1
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/listener_test.go
@@ -0,0 +1,234 @@
+package v3_test
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Listener modifications", func() {
+
+	type testCase struct {
+		listeners     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, listenerYAML := range given.listeners {
+				listener := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(listenerYAML), listener)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     listener.Name,
+					Origin:   generator.OriginInbound,
+					Resource: listener,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add listener", testCase{
+			modifications: []string{`
+                listener:
+                   operation: add
+                   value: |
+                     name: inbound:192.168.0.1:8080
+                     trafficDirection: INBOUND
+                     address:
+                       socketAddress:
+                         address: 192.168.0.1
+                         portValue: 8080
+                     filterChains:
+                     - filters:
+                       - name: envoy.filters.network.tcp_proxy
+                         typedConfig:
+                           '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                           cluster: localhost:8080
+                           statPrefix: localhost_8080`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: localhost:8080
+                      statPrefix: localhost_8080
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should replace listener", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: add
+                   value: |
+                     name: inbound:192.168.0.1:8080
+                     address:
+                       socketAddress:
+                         address: 192.168.0.2
+                         portValue: 8090`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                name: inbound:192.168.0.1:8080
+                address:
+                  socketAddress:
+                    address: 192.168.0.2
+                    portValue: 8090`,
+		}),
+		Entry("should remove listener matching all", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: remove`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should remove listener matching name", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+				`
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: remove
+                   match:
+                     name: inbound:192.168.0.1:8080`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8081
+                name: inbound:192.168.0.1:8081
+                trafficDirection: INBOUND`,
+		}),
+		Entry("should remove all inbound listeners", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: remove
+                   match:
+                     origin: inbound`,
+			},
+			expected: `{}`,
+		}),
+		Entry("should patch listener matching name", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{
+				`
+                listener:
+                   operation: patch
+                   match:
+                     name: inbound:192.168.0.1:8080
+                   value: |
+                     tcpFastOpenQueueLength: 32`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                name: inbound:192.168.0.1:8080
+                tcpFastOpenQueueLength: 32
+                trafficDirection: INBOUND`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/modifications_suite_test.go b/pkg/xds/generator/modifications/v3/modifications_suite_test.go
new file mode 100644
index 000000000..4d5dcfa53
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/modifications_suite_test.go
@@ -0,0 +1,19 @@
+package v3_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_test "github.com/kumahq/kuma/pkg/util/test"
+	_ "github.com/kumahq/kuma/pkg/xds/envoy"
+)
+
+func TestModifications(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Modifications V3 Suite",
+		[]Reporter{util_test.NewlineReporter{}})
+}
diff --git a/pkg/xds/generator/modifications/v3/network_filter_test.go b/pkg/xds/generator/modifications/v3/network_filter_test.go
new file mode 100644
index 000000000..a7c61f481
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/network_filter_test.go
@@ -0,0 +1,617 @@
+package v3_test
+
+import (
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Network Filter modifications", func() {
+
+	type testCase struct {
+		listeners     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, listenerYAML := range given.listeners {
+				listener := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(listenerYAML), listener)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     listener.Name,
+					Origin:   generator.OriginInbound,
+					Resource: listener,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should not add filter when there is no filter match", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addFirst
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter as a first", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addFirst
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should remove all filters from all listeners when there is no match section", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should remove all filters from all listeners when there is inbound match section", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+                   match:
+                     origin: inbound
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should remove all filters from picked listener", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+                   match:
+                     listenerName: inbound:192.168.0.1:8080
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should remove all filters of given name from all listeners", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+				`
+                name: inbound:192.168.0.1:8081
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: remove
+                   match:
+                     name: envoy.filters.network.tcp_proxy
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                name: inbound:192.168.0.1:8080
+            - name: inbound:192.168.0.1:8081
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8081`,
+		}),
+		Entry("should add filter after already defined (last)", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter after already defined", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.echo
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                  - name: envoy.filters.network.echo
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should not add filter when name is not matched", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - {}`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addAfter
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - {}
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter before already defined (first)", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addBefore
+                   match:
+                     name: envoy.filters.network.direct_response
+                   value: |
+                     name: envoy.filters.network.tcp_proxy
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should add filter before already defined", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: "xyz"
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                       cluster: backend
+`,
+			},
+			modifications: []string{`
+                networkFilter:
+                   operation: addBefore
+                   match:
+                     name: envoy.filters.network.tcp_proxy
+                   value: |
+                     name: envoy.filters.network.echo
+                     typedConfig:
+                       '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.direct_response
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.direct_response.v2.Config
+                      response:
+                        inlineString: xyz
+                  - name: envoy.filters.network.echo
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.echo.v2.Echo
+                  - name: envoy.filters.network.tcp_proxy
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
+                      cluster: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should patch resource matching filter name", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      stat_prefix: backend
+                      rds:
+                        configSource:
+                          ads: {}
+                        routeConfigName: outbound:backend
+                      httpFilters:
+                      - name: router
+`,
+			},
+			modifications: []string{`
+               networkFilter:
+                 operation: patch
+                 match:
+                   name: envoy.filters.network.http_connection_manager
+                 value: |
+                   name: envoy.filters.network.http_connection_manager
+                   typedConfig:
+                     '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                     streamIdleTimeout: 5s
+                     requestTimeout: 2s
+                     drainTimeout: 10s`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      drainTimeout: 10s
+                      httpFilters:
+                      - name: router
+                      rds:
+                        configSource:
+                          ads: {}
+                        routeConfigName: outbound:backend
+                      requestTimeout: 2s
+                      statPrefix: backend
+                      streamIdleTimeout: 5s
+                name: inbound:192.168.0.1:8080`,
+		}),
+		Entry("should patch resource providing config", testCase{
+			listeners: []string{
+				`
+                name: inbound:192.168.0.1:8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+`,
+			},
+			modifications: []string{`
+               networkFilter:
+                 operation: patch
+                 match:
+                   name: envoy.filters.network.http_connection_manager
+                 value: |
+                   name: envoy.filters.network.http_connection_manager
+                   typedConfig:
+                     '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                     statPrefix: backend`,
+			},
+			expected: `
+            resources:
+            - name: inbound:192.168.0.1:8080
+              resource:
+                '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: backend
+                name: inbound:192.168.0.1:8080`,
+		}),
+	)
+})
diff --git a/pkg/xds/generator/modifications/v3/virtual_host_test.go b/pkg/xds/generator/modifications/v3/virtual_host_test.go
new file mode 100644
index 000000000..517502e45
--- /dev/null
+++ b/pkg/xds/generator/modifications/v3/virtual_host_test.go
@@ -0,0 +1,335 @@
+package v3_test
+
+import (
+	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/generator"
+	modifications "github.com/kumahq/kuma/pkg/xds/generator/modifications/v3"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Virtual Host modifications", func() {
+
+	type testCase struct {
+		routeCfgs     []string
+		modifications []string
+		expected      string
+	}
+
+	DescribeTable("should apply modifications",
+		func(given testCase) {
+			// given
+			set := core_xds.NewResourceSet()
+			for _, routeCfgYAML := range given.routeCfgs {
+				routeCfg := &envoy_listener.Listener{}
+				err := util_proto.FromYAML([]byte(routeCfgYAML), routeCfg)
+				Expect(err).ToNot(HaveOccurred())
+				set.Add(&core_xds.Resource{
+					Name:     routeCfg.Name,
+					Origin:   generator.OriginOutbound,
+					Resource: routeCfg,
+				})
+			}
+
+			var mods []*mesh_proto.ProxyTemplate_Modifications
+			for _, modificationYAML := range given.modifications {
+				modification := &mesh_proto.ProxyTemplate_Modifications{}
+				err := util_proto.FromYAML([]byte(modificationYAML), modification)
+				Expect(err).ToNot(HaveOccurred())
+				mods = append(mods, modification)
+			}
+
+			// when
+			err := modifications.Apply(set, mods)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			resp, err := set.List().ToDeltaDiscoveryResponse()
+			Expect(err).ToNot(HaveOccurred())
+			actual, err := util_proto.ToYAML(resp)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("should add virtual host", testCase{
+			routeCfgs: []string{
+				`
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                filterChains:
+                - filters:
+                  - name: envoy.filters.network.http_connection_manager
+                    typedConfig:
+                      '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+                      statPrefix: localhost_8080
+                      httpFilters:
+                      - name: envoy.filters.http.router
+                      routeConfig:
+                        name: outbound:backend
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: add
+                   value: |
+                     name: backend
+                     domains:
+                     - backend.com
+                     routes:
+                     - match:
+                         prefix: /
+                       route:
+                         cluster: backend`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    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
+                          routeConfig:
+                            name: outbound:backend
+                            virtualHosts:
+                            - domains:
+                              - backend.com
+                              name: backend
+                              routes:
+                              - match:
+                                  prefix: /
+                                route:
+                                  cluster: backend
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND
+`,
+		}),
+		Entry("should remove virtual host", testCase{
+			routeCfgs: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      routeConfig:
+                        name: outbound:backend
+                        virtualHosts:
+                        - domains:
+                          - backend.com
+                          name: backend
+                          routes:
+                          - match:
+                              prefix: /
+                            route:
+                              cluster: backend
+                      statPrefix: localhost_8080
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: remove
+                   match:
+                     name: backend`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    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
+                          routeConfig:
+                            name: outbound:backend
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND`,
+		}),
+		Entry("should patch a virtual host", testCase{
+			routeCfgs: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      routeConfig:
+                        name: outbound:backend
+                        virtualHosts:
+                        - domains:
+                          - backend.com
+                          name: backend
+                          routes:
+                          - match:
+                              prefix: /
+                            route:
+                              cluster: backend
+                      statPrefix: localhost_8080
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: patch
+                   match:
+                     origin: outbound
+                   value: |
+                     retryPolicy:
+                       retryOn: 5xx
+                       numRetries: 3`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    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
+                          routeConfig:
+                            name: outbound:backend
+                            virtualHosts:
+                            - domains:
+                              - backend.com
+                              name: backend
+                              retryPolicy:
+                                numRetries: 3
+                                retryOn: 5xx
+                              routes:
+                              - match:
+                                  prefix: /
+                                route:
+                                  cluster: backend
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND`,
+		}),
+		Entry("should patch a virtual host adding new route", testCase{
+			routeCfgs: []string{
+				`
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+                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
+                      routeConfig:
+                        name: outbound:backend
+                        virtualHosts:
+                        - domains:
+                          - backend.com
+                          name: backend
+                          routes:
+                          - match:
+                              prefix: /
+                            route:
+                              cluster: backend
+                      statPrefix: localhost_8080
+                name: outbound:192.168.0.1:8080
+                trafficDirection: INBOUND
+`,
+			},
+			modifications: []string{`
+                virtualHost:
+                   operation: patch
+                   match:
+                     routeConfigurationName: outbound:backend
+                   value: |
+                     routes:
+                     - match:
+                         prefix: /web
+                       route:
+                         cluster: web`,
+			},
+			expected: `
+                resources:
+                - name: outbound:192.168.0.1:8080
+                  resource:
+                    '@type': type.googleapis.com/envoy.config.listener.v3.Listener
+                    address:
+                      socketAddress:
+                        address: 192.168.0.1
+                        portValue: 8080
+                    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
+                          routeConfig:
+                            name: outbound:backend
+                            virtualHosts:
+                            - domains:
+                              - backend.com
+                              name: backend
+                              routes:
+                              - match:
+                                  prefix: /
+                                route:
+                                  cluster: backend
+                              - match:
+                                  prefix: /web
+                                route:
+                                  cluster: web
+                          statPrefix: localhost_8080
+                    name: outbound:192.168.0.1:8080
+                    trafficDirection: INBOUND`,
+		}),
+	)
+})