Repository: kumahq/kuma
Pull request #1322 (2020-12-16 12:38:54)

v3 migration - clusters, endpoints (bp #1312)
This is an automated backport of pull request #1312 done by Mergify

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

You are currently cherry-picking commit 9df49a19.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:

	modified:   pkg/core/xds/types.go
	modified:   pkg/util/proto/any_test.go
	modified:   pkg/xds/cache/cla/cache.go
	new file:   pkg/xds/envoy/api_version.go
	modified:   pkg/xds/envoy/clusters/cluster_builder.go
	new file:   pkg/xds/envoy/clusters/configurers.go
	deleted:    pkg/xds/envoy/clusters/dns_cluster_configurer.go
	deleted:    pkg/xds/envoy/clusters/health_check_configurer.go
	deleted:    pkg/xds/envoy/clusters/http2_configurer.go
	deleted:    pkg/xds/envoy/clusters/lb_subset_configurer.go
	deleted:    pkg/xds/envoy/clusters/pass_through_cluster_configurer.go
	deleted:    pkg/xds/envoy/clusters/static_cluster_configurer.go
	deleted:    pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go
	deleted:    pkg/xds/envoy/clusters/upstream_bind_config_configurer.go
	renamed:    pkg/xds/envoy/clusters/alt_stat_name_configurer.go -> pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
	renamed:    pkg/xds/envoy/clusters/circuit_breaker_configurer.go -> pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
	new file:   pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go
	renamed:    pkg/xds/envoy/clusters/client_side_mtls_configurer.go -> pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
	renamed:    pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go -> pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
	renamed:    pkg/xds/envoy/clusters/client_side_tls_configurer.go -> pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
	renamed:    pkg/xds/envoy/clusters/client_side_tls_configurer_test.go -> pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
	renamed:    pkg/xds/envoy/clusters/clusters_suite_test.go -> pkg/xds/envoy/clusters/v2/clusters_suite_test.go
	new file:   pkg/xds/envoy/clusters/v2/configurer.go
	new file:   pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go
	renamed:    pkg/xds/envoy/clusters/dns_cluster_configurer_test.go -> pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
	renamed:    pkg/xds/envoy/clusters/eds_cluster_configurer.go -> pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
	renamed:    pkg/xds/envoy/clusters/eds_cluster_configurer_test.go -> pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
	renamed:    pkg/xds/envoy/clusters/health_check_configurer_test.go -> pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v2/http2_configurer.go
	renamed:    pkg/xds/envoy/clusters/http2_configurer_test.go -> pkg/xds/envoy/clusters/v2/http2_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v2/lb_subset_configurer.go
	renamed:    pkg/xds/envoy/clusters/lb_subset_configurer_test.go -> pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go
	renamed:    pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go -> pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v2/static_cluster_configurer.go
	renamed:    pkg/xds/envoy/clusters/static_cluster_configurer_test.go -> pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go
	renamed:    pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go -> pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
	renamed:    pkg/xds/envoy/clusters/timeout_configurer.go -> pkg/xds/envoy/clusters/v2/timeout_configurer.go
	new file:   pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go
	renamed:    pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go -> pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/clusters_suite_test.go
	new file:   pkg/xds/envoy/clusters/v3/configurer.go
	new file:   pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/health_check_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/http2_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/http2_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/lb_subset_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/static_cluster_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go
	new file:   pkg/xds/envoy/clusters/v3/timeout_configurer.go
	new file:   pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go
	renamed:    pkg/xds/envoy/endpoints/endpoints.go -> pkg/xds/envoy/endpoints/v2/endpoints.go
	renamed:    pkg/xds/envoy/endpoints/endpoints_suite_test.go -> pkg/xds/envoy/endpoints/v2/endpoints_suite_test.go
	renamed:    pkg/xds/envoy/endpoints/endpoints_test.go -> pkg/xds/envoy/endpoints/v2/endpoints_test.go
	new file:   pkg/xds/envoy/endpoints/v3/endpoints.go
	new file:   pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go
	new file:   pkg/xds/envoy/endpoints/v3/endpoints_test.go
	modified:   pkg/xds/envoy/listeners/network_rbac_configurer.go
	modified:   pkg/xds/envoy/listeners/server_mtls_configurer.go
	modified:   pkg/xds/envoy/listeners/tcp_proxy_configurer.go
	renamed:    pkg/xds/envoy/metadata.go -> pkg/xds/envoy/metadata/v2/metadata.go
	new file:   pkg/xds/envoy/metadata/v2/metadata_suite_test.go
	renamed:    pkg/xds/envoy/metadata_test.go -> pkg/xds/envoy/metadata/v2/metadata_test.go
	new file:   pkg/xds/envoy/metadata/v3/metadata.go
	new file:   pkg/xds/envoy/metadata/v3/metadata_suite_test.go
	new file:   pkg/xds/envoy/metadata/v3/metadata_test.go
	modified:   pkg/xds/envoy/routes/default_route_configurer.go
	modified:   pkg/xds/envoy/tls/tls.go
	new file:   pkg/xds/envoy/tls/v2/tls.go
	renamed:    pkg/xds/envoy/tls/tls_suite_test.go -> pkg/xds/envoy/tls/v2/tls_suite_test.go
	renamed:    pkg/xds/envoy/tls/tls_test.go -> pkg/xds/envoy/tls/v2/tls_test.go
	new file:   pkg/xds/envoy/tls/v3/tls.go
	new file:   pkg/xds/envoy/tls/v3/tls_suite_test.go
	new file:   pkg/xds/envoy/tls/v3/tls_test.go
	modified:   pkg/xds/envoy/types.go
	modified:   pkg/xds/generator/direct_access_proxy_generator.go
	modified:   pkg/xds/generator/inbound_proxy_generator.go
	modified:   pkg/xds/generator/ingress_generator.go
	modified:   pkg/xds/generator/outbound_proxy_generator.go
	modified:   pkg/xds/generator/prometheus_endpoint_generator.go
	modified:   pkg/xds/generator/proxy_template_profile_source_test.go
	modified:   pkg/xds/generator/tracing_proxy_generator.go
	modified:   pkg/xds/generator/transparent_proxy_generator.go
	modified:   pkg/xds/topology/outbound.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)

	both modified:   pkg/xds/envoy/clusters/v2/health_check_configurer.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/types.go b/pkg/core/xds/types.go
index 40839b2d5..ba234e91c 100644
--- a/pkg/core/xds/types.go
+++ b/pkg/core/xds/types.go
@@ -44,9 +44,9 @@ type DestinationMap map[ServiceName]TagSelectorSet
 
 type ExternalService struct {
 	TLSEnabled bool
-	CaCert     *envoy_core.DataSource
-	ClientCert *envoy_core.DataSource
-	ClientKey  *envoy_core.DataSource
+	CaCert     []byte
+	ClientCert []byte
+	ClientKey  []byte
 }
 
 type Locality struct {
diff --git a/pkg/plugins/runtime/k8s/probes/probe.go b/pkg/plugins/runtime/k8s/probes/probe.go
index 828de9b79..a02c28b68 100644
--- a/pkg/plugins/runtime/k8s/probes/probe.go
+++ b/pkg/plugins/runtime/k8s/probes/probe.go
@@ -51,11 +51,15 @@ func (p KumaProbe) ToVirtual(virtualPort uint32) (KumaProbe, error) {
 		return KumaProbe{}, errors.Errorf("cannot override Pod's probes. Port for probe cannot "+
 			"be set to %d. It is reserved for the dataplane that will serve pods without mTLS.", virtualPort)
 	}
+	probePath := p.Path()
+	if !strings.HasPrefix(p.Path(), "/") {
+		probePath = fmt.Sprintf("/%s", p.Path())
+	}
 	return KumaProbe{
 		Handler: kube_core.Handler{
 			HTTPGet: &kube_core.HTTPGetAction{
 				Port: intstr.FromInt(int(virtualPort)),
-				Path: fmt.Sprintf("/%d%s", p.Port(), p.Path()),
+				Path: fmt.Sprintf("/%d%s", p.Port(), probePath),
 			},
 		},
 	}, nil
diff --git a/pkg/xds/cache/cla/cache.go b/pkg/xds/cache/cla/cache.go
index 631076999..5504342f3 100644
--- a/pkg/xds/cache/cla/cache.go
+++ b/pkg/xds/cache/cla/cache.go
@@ -21,7 +21,7 @@ import (
 	"github.com/kumahq/kuma/pkg/core/resources/manager"
 	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
 	"github.com/kumahq/kuma/pkg/xds/cache/once"
-	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 	"github.com/kumahq/kuma/pkg/xds/topology"
 )
 
diff --git a/pkg/xds/envoy/api_version.go b/pkg/xds/envoy/api_version.go
new file mode 100644
index 000000000..c53d11c3a
--- /dev/null
+++ b/pkg/xds/envoy/api_version.go
@@ -0,0 +1,8 @@
+package envoy
+
+type APIVersion string
+
+const (
+	APIV2 APIVersion = "v2"
+	APIV3 APIVersion = "v3"
+)
diff --git a/pkg/xds/envoy/clusters/cluster_builder.go b/pkg/xds/envoy/clusters/cluster_builder.go
index 9a6fcf0c9..b8fddbcf7 100644
--- a/pkg/xds/envoy/clusters/cluster_builder.go
+++ b/pkg/xds/envoy/clusters/cluster_builder.go
@@ -1,15 +1,15 @@
 package clusters
 
 import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-)
+	"errors"
 
-// ClusterConfigurer is responsible for configuring a single aspect of the entire Envoy cluster,
-// such as filter chain, transparent proxying, etc.
-type ClusterConfigurer interface {
-	// Configure configures a single aspect on a given Envoy cluster.
-	Configure(cluster *envoy_api.Cluster) error
-}
+	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	v2 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v2"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v3"
+)
 
 // ClusterBuilderOpt is a configuration option for ClusterBuilder.
 //
@@ -19,14 +19,17 @@ type ClusterBuilderOpt interface {
 	ApplyTo(config *ClusterBuilderConfig)
 }
 
-func NewClusterBuilder() *ClusterBuilder {
-	return &ClusterBuilder{}
+func NewClusterBuilder(apiVersion envoy.APIVersion) *ClusterBuilder {
+	return &ClusterBuilder{
+		apiVersion: apiVersion,
+	}
 }
 
 // ClusterBuilder is responsible for generating an Envoy cluster
 // by applying a series of ClusterConfigurers.
 type ClusterBuilder struct {
-	config ClusterBuilderConfig
+	apiVersion envoy.APIVersion
+	config     ClusterBuilderConfig
 }
 
 // Configure configures ClusterBuilder by adding individual ClusterConfigurers.
@@ -38,25 +41,44 @@ func (b *ClusterBuilder) Configure(opts ...ClusterBuilderOpt) *ClusterBuilder {
 }
 
 // Build generates an Envoy cluster by applying a series of ClusterConfigurers.
-func (b *ClusterBuilder) Build() (*envoy_api.Cluster, error) {
-	cluster := envoy_api.Cluster{}
-	for _, configurer := range b.config.Configurers {
-		if err := configurer.Configure(&cluster); err != nil {
-			return nil, err
+func (b *ClusterBuilder) Build() (envoy.NamedResource, error) {
+	switch b.apiVersion {
+	case envoy.APIV2:
+		cluster := envoy_api_v2.Cluster{}
+		for _, configurer := range b.config.ConfigurersV2 {
+			if err := configurer.Configure(&cluster); err != nil {
+				return nil, err
+			}
 		}
+		return &cluster, nil
+	case envoy.APIV3:
+		cluster := envoy_api.Cluster{}
+		for _, configurer := range b.config.ConfigurersV3 {
+			if err := configurer.Configure(&cluster); err != nil {
+				return nil, err
+			}
+		}
+		return &cluster, nil
+	default:
+		return nil, errors.New("unknown API")
 	}
-	return &cluster, nil
 }
 
 // ClusterBuilderConfig holds configuration of a ClusterBuilder.
 type ClusterBuilderConfig struct {
 	// A series of ClusterConfigurers to apply to Envoy cluster.
-	Configurers []ClusterConfigurer
+	ConfigurersV2 []v2.ClusterConfigurer
+	ConfigurersV3 []v3.ClusterConfigurer
+}
+
+// Add appends a given ClusterConfigurer to the end of the chain.
+func (c *ClusterBuilderConfig) AddV2(configurer v2.ClusterConfigurer) {
+	c.ConfigurersV2 = append(c.ConfigurersV2, configurer)
 }
 
 // Add appends a given ClusterConfigurer to the end of the chain.
-func (c *ClusterBuilderConfig) Add(configurer ClusterConfigurer) {
-	c.Configurers = append(c.Configurers, configurer)
+func (c *ClusterBuilderConfig) AddV3(configurer v3.ClusterConfigurer) {
+	c.ConfigurersV3 = append(c.ConfigurersV3, configurer)
 }
 
 // ClusterBuilderOptFunc is a convenience type adapter.
diff --git a/pkg/xds/envoy/clusters/configurers.go b/pkg/xds/envoy/clusters/configurers.go
new file mode 100644
index 000000000..da29a0543
--- /dev/null
+++ b/pkg/xds/envoy/clusters/configurers.go
@@ -0,0 +1,206 @@
+package clusters
+
+import (
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	v2 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v2"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/clusters/v3"
+)
+
+func OutlierDetection(circuitBreaker *mesh_core.CircuitBreakerResource) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.OutlierDetectionConfigurer{CircuitBreaker: circuitBreaker})
+		config.AddV3(&v3.OutlierDetectionConfigurer{CircuitBreaker: circuitBreaker})
+	})
+}
+
+func ClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, clientService string, tags []envoy.Tags) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: clientService,
+			Tags:          tags,
+		})
+		config.AddV3(&v3.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: clientService,
+			Tags:          tags,
+		})
+	})
+}
+
+// UnknownDestinationClientSideMTLS configures cluster with mTLS for a mesh but without extensive destination verification (only Mesh is verified)
+func UnknownDestinationClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: "*",
+			Tags:          nil,
+		})
+		config.AddV3(&v3.ClientSideMTLSConfigurer{
+			Ctx:           ctx,
+			Metadata:      metadata,
+			ClientService: "*",
+			Tags:          nil,
+		})
+	})
+}
+
+func ClientSideTLS(endpoints []core_xds.Endpoint) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.ClientSideTLSConfigurer{
+			Endpoints: endpoints,
+		})
+		config.AddV3(&v3.ClientSideTLSConfigurer{
+			Endpoints: endpoints,
+		})
+	})
+}
+
+func DNSCluster(name string, address string, port uint32) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.DnsClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.DnsClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func EdsCluster(name string) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.EdsClusterConfigurer{
+			Name: name,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.EdsClusterConfigurer{
+			Name: name,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func HealthCheck(healthCheck *mesh_core.HealthCheckResource) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.HealthCheckConfigurer{
+			HealthCheck: healthCheck,
+		})
+		config.AddV3(&v3.HealthCheckConfigurer{
+			HealthCheck: healthCheck,
+		})
+	})
+}
+
+// LbSubset is required for MetadataMatch in Weighted Cluster in TCP Proxy to work.
+// Subset loadbalancing is used in two use cases
+// 1) TrafficRoute for splitting traffic. Example: TrafficRoute that splits 10% of the traffic to version 1 of the service backend and 90% traffic to version 2 of the service backend
+// 2) Multiple outbound sections with the same service
+//    Example:
+//    type: Dataplane
+//    networking:
+//      outbound:
+//      - port: 1234
+//        tags:
+//          kuma.io/service: backend
+//      - port: 1234
+//        tags:
+//          kuma.io/service: backend
+//          version: v1
+//    Only one cluster "backend" is generated for such dataplane, but with lb subset by version.
+func LbSubset(keySets [][]string) ClusterBuilderOptFunc {
+	return func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.LbSubsetConfigurer{
+			KeySets: keySets,
+		})
+		config.AddV3(&v3.LbSubsetConfigurer{
+			KeySets: keySets,
+		})
+	}
+}
+
+func PassThroughCluster(name string) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.PassThroughClusterConfigurer{
+			Name: name,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.PassThroughClusterConfigurer{
+			Name: name,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func StaticCluster(name string, address string, port uint32) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.StaticClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.StaticClusterConfigurer{
+			Name:    name,
+			Address: address,
+			Port:    port,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func StrictDNSCluster(name string, endpoints []core_xds.Endpoint) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.StrictDNSClusterConfigurer{
+			Name:      name,
+			Endpoints: endpoints,
+		})
+		config.AddV2(&v2.AltStatNameConfigurer{})
+		config.AddV2(&v2.TimeoutConfigurer{})
+		config.AddV3(&v3.StrictDNSClusterConfigurer{
+			Name:      name,
+			Endpoints: endpoints,
+		})
+		config.AddV3(&v3.AltStatNameConfigurer{})
+		config.AddV3(&v3.TimeoutConfigurer{})
+	})
+}
+
+func UpstreamBindConfig(address string, port uint32) ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.UpstreamBindConfigConfigurer{
+			Address: address,
+			Port:    port,
+		})
+		config.AddV3(&v3.UpstreamBindConfigConfigurer{
+			Address: address,
+			Port:    port,
+		})
+	})
+}
+
+func Http2() ClusterBuilderOpt {
+	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
+		config.AddV2(&v2.Http2Configurer{})
+		config.AddV3(&v3.Http2Configurer{})
+	})
+}
diff --git a/pkg/xds/envoy/clusters/dns_cluster_configurer.go b/pkg/xds/envoy/clusters/dns_cluster_configurer.go
deleted file mode 100644
index 4def95243..000000000
--- a/pkg/xds/envoy/clusters/dns_cluster_configurer.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
-)
-
-func DNSCluster(name string, address string, port uint32) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&dnsClusterConfigurer{
-			name:    name,
-			address: address,
-			port:    port,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type dnsClusterConfigurer struct {
-	name    string
-	address string
-	port    uint32
-}
-
-func (e *dnsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = e.name
-	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
-	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
-	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.name, e.address, e.port)
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/http2_configurer.go b/pkg/xds/envoy/clusters/http2_configurer.go
deleted file mode 100644
index f7ecf74ca..000000000
--- a/pkg/xds/envoy/clusters/http2_configurer.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-)
-
-func Http2() ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&http2Configurer{})
-	})
-}
-
-type http2Configurer struct {
-}
-
-func (p *http2Configurer) Configure(c *envoy_api.Cluster) error {
-	c.Http2ProtocolOptions = &envoy_api_v2_core.Http2ProtocolOptions{}
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/lb_subset_configurer.go b/pkg/xds/envoy/clusters/lb_subset_configurer.go
deleted file mode 100644
index b558da571..000000000
--- a/pkg/xds/envoy/clusters/lb_subset_configurer.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-)
-
-// LbSubset is required for MetadataMatch in Weighted Cluster in TCP Proxy to work.
-// Subset loadbalancing is used in two use cases
-// 1) TrafficRoute for splitting traffic. Example: TrafficRoute that splits 10% of the traffic to version 1 of the service backend and 90% traffic to version 2 of the service backend
-// 2) Multiple outbound sections with the same service
-//    Example:
-//    type: Dataplane
-//    networking:
-//      outbound:
-//      - port: 1234
-//        tags:
-//          kuma.io/service: backend
-//      - port: 1234
-//        tags:
-//          kuma.io/service: backend
-//          version: v1
-//    Only one cluster "backend" is generated for such dataplane, but with lb subset by version.
-func LbSubset(keySets [][]string) ClusterBuilderOptFunc {
-	return func(config *ClusterBuilderConfig) {
-		config.Add(&lbSubsetConfigurer{
-			keySets: keySets,
-		})
-	}
-}
-
-type lbSubsetConfigurer struct {
-	keySets [][]string
-}
-
-func (e *lbSubsetConfigurer) Configure(c *envoy_api.Cluster) error {
-	var selectors []*envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector
-	for _, keys := range e.keySets {
-		if len(keys) == 0 {
-			continue
-		}
-		selectors = append(selectors, &envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector{
-			Keys: keys,
-			// if there is a split by "version", and there is no endpoint with such version we should not fallback to all endpoints of the service
-			FallbackPolicy: envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector_NO_FALLBACK,
-		})
-	}
-	if len(selectors) > 0 {
-		// if lb subset is set, but no label (Kuma's tag) is queried, we should return any endpoint
-		c.LbSubsetConfig = &envoy_api.Cluster_LbSubsetConfig{
-			FallbackPolicy:  envoy_api.Cluster_LbSubsetConfig_ANY_ENDPOINT,
-			SubsetSelectors: selectors,
-		}
-	}
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/pass_through_cluster_configurer.go b/pkg/xds/envoy/clusters/pass_through_cluster_configurer.go
deleted file mode 100644
index a88944613..000000000
--- a/pkg/xds/envoy/clusters/pass_through_cluster_configurer.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-)
-
-func PassThroughCluster(name string) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&passThroughClusterConfigurer{
-			name: name,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type passThroughClusterConfigurer struct {
-	name string
-}
-
-func (p *passThroughClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = p.name
-	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_ORIGINAL_DST}
-	c.LbPolicy = envoy_api.Cluster_CLUSTER_PROVIDED
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/static_cluster_configurer.go b/pkg/xds/envoy/clusters/static_cluster_configurer.go
deleted file mode 100644
index cb903f2e7..000000000
--- a/pkg/xds/envoy/clusters/static_cluster_configurer.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package clusters
-
-import (
-	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
-)
-
-func StaticCluster(name string, address string, port uint32) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&staticClusterConfigurer{
-			name:    name,
-			address: address,
-			port:    port,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type staticClusterConfigurer struct {
-	name    string
-	address string
-	port    uint32
-}
-
-func (e *staticClusterConfigurer) Configure(c *v2.Cluster) error {
-	c.Name = e.name
-	c.ClusterDiscoveryType = &v2.Cluster_Type{Type: v2.Cluster_STATIC}
-	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.name, e.address, e.port)
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go b/pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go
deleted file mode 100644
index cabb22ac2..000000000
--- a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-
-	"github.com/kumahq/kuma/pkg/core/xds"
-
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
-)
-
-func StrictDNSCluster(name string, endpoints []xds.Endpoint) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&StricDNSClusterConfigurer{
-			name:      name,
-			endpoints: endpoints,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
-}
-
-type StricDNSClusterConfigurer struct {
-	name      string
-	endpoints []xds.Endpoint
-}
-
-func (e *StricDNSClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = e.name
-	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
-	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
-	c.LoadAssignment = envoy_endpoints.CreateClusterLoadAssignment(e.name, e.endpoints)
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/upstream_bind_config_configurer.go b/pkg/xds/envoy/clusters/upstream_bind_config_configurer.go
deleted file mode 100644
index eae6ede7e..000000000
--- a/pkg/xds/envoy/clusters/upstream_bind_config_configurer.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package clusters
-
-import (
-	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-)
-
-func UpstreamBindConfig(address string, port uint32) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&upstreamBindConfigConfigurer{
-			address: address,
-			port:    port,
-		})
-	})
-}
-
-type upstreamBindConfigConfigurer struct {
-	address string
-	port    uint32
-}
-
-func (u *upstreamBindConfigConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.UpstreamBindConfig = &envoy_core.BindConfig{
-		SourceAddress: &envoy_core.SocketAddress{
-			Address: u.address,
-			PortSpecifier: &envoy_core.SocketAddress_PortValue{
-				PortValue: u.port,
-			},
-		},
-	}
-	return nil
-}
diff --git a/pkg/xds/envoy/clusters/alt_stat_name_configurer.go b/pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
similarity index 55%
rename from pkg/xds/envoy/clusters/alt_stat_name_configurer.go
rename to pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
index af2ee641a..b98c6c15e 100644
--- a/pkg/xds/envoy/clusters/alt_stat_name_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/alt_stat_name_configurer.go
@@ -6,16 +6,12 @@ import (
 	util_xds "github.com/kumahq/kuma/pkg/util/xds"
 )
 
-func AltStatName() ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&altStatNameConfigurer{})
-	})
+type AltStatNameConfigurer struct {
 }
 
-type altStatNameConfigurer struct {
-}
+var _ ClusterConfigurer = &AltStatNameConfigurer{}
 
-func (e *altStatNameConfigurer) Configure(cluster *envoy_api.Cluster) error {
+func (e *AltStatNameConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	sanitizedName := util_xds.SanitizeMetric(cluster.Name)
 	if sanitizedName != cluster.Name {
 		cluster.AltStatName = sanitizedName
diff --git a/pkg/xds/envoy/clusters/circuit_breaker_configurer.go b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
similarity index 94%
rename from pkg/xds/envoy/clusters/circuit_breaker_configurer.go
rename to pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
index 1cb12b37c..c6afcca06 100644
--- a/pkg/xds/envoy/clusters/circuit_breaker_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer.go
@@ -8,18 +8,12 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 )
 
-func OutlierDetection(circuitBreaker *mesh_core.CircuitBreakerResource) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&OutlierDetectionConfigurer{
-			CircuitBreaker: circuitBreaker,
-		})
-	})
-}
-
 type OutlierDetectionConfigurer struct {
 	CircuitBreaker *mesh_core.CircuitBreakerResource
 }
 
+var _ ClusterConfigurer = &OutlierDetectionConfigurer{}
+
 func (c *OutlierDetectionConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	if c.CircuitBreaker == nil {
 		return nil
diff --git a/pkg/xds/envoy/clusters/client_side_mtls_configurer.go b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
similarity index 56%
rename from pkg/xds/envoy/clusters/client_side_mtls_configurer.go
rename to pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
index 2f59a6c4e..965eb3ff1 100644
--- a/pkg/xds/envoy/clusters/client_side_mtls_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer.go
@@ -9,46 +9,27 @@ import (
 	"github.com/kumahq/kuma/pkg/util/proto"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls_v2 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 )
 
-func ClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, clientService string, tags []envoy.Tags) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&clientSideMTLSConfigurer{
-			ctx:           ctx,
-			metadata:      metadata,
-			clientService: clientService,
-			tags:          tags,
-		})
-	})
+type ClientSideMTLSConfigurer struct {
+	Ctx           xds_context.Context
+	Metadata      *core_xds.DataplaneMetadata
+	ClientService string
+	Tags          []envoy.Tags
 }
 
-// UnknownDestinationClientSideMTLS configures cluster with mTLS for a mesh but without extensive destination verification (only Mesh is verified)
-func UnknownDestinationClientSideMTLS(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&clientSideMTLSConfigurer{
-			ctx:           ctx,
-			metadata:      metadata,
-			clientService: "*",
-			tags:          nil,
-		})
-	})
-}
-
-type clientSideMTLSConfigurer struct {
-	ctx           xds_context.Context
-	metadata      *core_xds.DataplaneMetadata
-	clientService string
-	tags          []envoy.Tags
-}
+var _ ClusterConfigurer = &ClientSideMTLSConfigurer{}
 
-func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
-	if !c.ctx.Mesh.Resource.MTLSEnabled() {
+func (c *ClientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
+	if !c.Ctx.Mesh.Resource.MTLSEnabled() {
 		return nil
 	}
-	mesh := c.ctx.Mesh.Resource.GetMeta().GetName()
+	mesh := c.Ctx.Mesh.Resource.GetMeta().GetName()
 	// there might be a situation when there are multiple sam tags passed here for example two outbound listeners with the same tags, therefore we need to distinguish between them.
-	distinctTags := envoy.DistinctTags(c.tags)
+	distinctTags := envoy.DistinctTags(c.Tags)
 	switch {
 	case len(distinctTags) == 0:
 		transportSocket, err := c.createTransportSocket("")
@@ -57,7 +38,7 @@ func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 		}
 		cluster.TransportSocket = transportSocket
 	case len(distinctTags) == 1:
-		transportSocket, err := c.createTransportSocket(tls.SNIFromTags(c.tags[0].WithTags("mesh", mesh)))
+		transportSocket, err := c.createTransportSocket(tls.SNIFromTags(c.Tags[0].WithTags("mesh", mesh)))
 		if err != nil {
 			return err
 		}
@@ -72,7 +53,7 @@ func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_api.Cluster_TransportSocketMatch{
 				Name: sni,
 				Match: &pstruct.Struct{
-					Fields: envoy.MetadataFields(tags),
+					Fields: envoy_metadata.MetadataFields(tags),
 				},
 				TransportSocket: transportSocket,
 			})
@@ -81,8 +62,8 @@ func (c *clientSideMTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	return nil
 }
 
-func (c *clientSideMTLSConfigurer) createTransportSocket(sni string) (*envoy_core.TransportSocket, error) {
-	tlsContext, err := tls.CreateUpstreamTlsContext(c.ctx, c.metadata, c.clientService, sni)
+func (c *ClientSideMTLSConfigurer) createTransportSocket(sni string) (*envoy_core.TransportSocket, error) {
+	tlsContext, err := tls_v2.CreateUpstreamTlsContext(c.Ctx, c.Metadata, c.ClientService, sni)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/xds/envoy/clusters/client_side_tls_configurer.go b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
similarity index 65%
rename from pkg/xds/envoy/clusters/client_side_tls_configurer.go
rename to pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
index c04e89d80..8ad81fe47 100644
--- a/pkg/xds/envoy/clusters/client_side_tls_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer.go
@@ -5,29 +5,22 @@ import (
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
 	pstruct "github.com/golang/protobuf/ptypes/struct"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
-
 	"github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/util/proto"
-	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
+	tls_v2 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 )
 
-func ClientSideTLS(endpoints []xds.Endpoint) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&clientSideTLSConfigurer{
-			endpoints: endpoints,
-		})
-	})
+type ClientSideTLSConfigurer struct {
+	Endpoints []xds.Endpoint
 }
 
-type clientSideTLSConfigurer struct {
-	endpoints []xds.Endpoint
-}
+var _ ClusterConfigurer = &ClientSideTLSConfigurer{}
 
-func (c *clientSideTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
-	for _, ep := range c.endpoints {
+func (c *ClientSideTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
+	for _, ep := range c.Endpoints {
 		if ep.ExternalService.TLSEnabled {
-			tlsContext, err := tls.UpstreamTlsContextOutsideMesh(
+			tlsContext, err := tls_v2.UpstreamTlsContextOutsideMesh(
 				ep.ExternalService.CaCert,
 				ep.ExternalService.ClientCert,
 				ep.ExternalService.ClientKey,
@@ -51,7 +44,7 @@ func (c *clientSideTLSConfigurer) Configure(cluster *envoy_api.Cluster) error {
 			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_api.Cluster_TransportSocketMatch{
 				Name: ep.Target,
 				Match: &pstruct.Struct{
-					Fields: envoy.MetadataFields(ep.Tags),
+					Fields: envoy_metadata.MetadataFields(ep.Tags),
 				},
 				TransportSocket: transportSocket,
 			})
diff --git a/pkg/xds/envoy/clusters/v2/configurer.go b/pkg/xds/envoy/clusters/v2/configurer.go
new file mode 100644
index 000000000..f34d8b43d
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/configurer.go
@@ -0,0 +1,10 @@
+package clusters
+
+import envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+// ClusterConfigurer is responsible for configuring a single aspect of the entire Envoy cluster,
+// such as filter chain, transparent proxying, etc.
+type ClusterConfigurer interface {
+	// Configure configures a single aspect on a given Envoy cluster.
+	Configure(cluster *envoy_api.Cluster) error
+}
diff --git a/pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go
new file mode 100644
index 000000000..282c2c982
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer.go
@@ -0,0 +1,23 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
+)
+
+type DnsClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &DnsClusterConfigurer{}
+
+func (e *DnsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
+	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/eds_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
similarity index 56%
rename from pkg/xds/envoy/clusters/eds_cluster_configurer.go
rename to pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
index abe008f75..cd596e606 100644
--- a/pkg/xds/envoy/clusters/eds_cluster_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer.go
@@ -5,22 +5,14 @@ import (
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
 )
 
-func EdsCluster(name string) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&edsClusterConfigurer{
-			name: name,
-		})
-		config.Add(&altStatNameConfigurer{})
-		config.Add(&timeoutConfigurer{})
-	})
+type EdsClusterConfigurer struct {
+	Name string
 }
 
-type edsClusterConfigurer struct {
-	name string
-}
+var _ ClusterConfigurer = &EdsClusterConfigurer{}
 
-func (e *edsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
-	c.Name = e.name
+func (e *EdsClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
 	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_EDS}
 	c.EdsClusterConfig = &envoy_api.Cluster_EdsClusterConfig{
 		EdsConfig: &envoy_core.ConfigSource{
diff --git a/pkg/xds/envoy/clusters/health_check_configurer.go b/pkg/xds/envoy/clusters/v2/health_check_configurer.go
similarity index 62%
rename from pkg/xds/envoy/clusters/health_check_configurer.go
rename to pkg/xds/envoy/clusters/v2/health_check_configurer.go
index d2ee8a58e..b4542ad59 100644
--- a/pkg/xds/envoy/clusters/health_check_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/health_check_configurer.go
@@ -8,23 +8,17 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 )
 
-func HealthCheck(healthCheck *mesh_core.HealthCheckResource) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&healthCheckConfigurer{
-			healthCheck: healthCheck,
-		})
-	})
+type HealthCheckConfigurer struct {
+	HealthCheck *mesh_core.HealthCheckResource
 }
 
-type healthCheckConfigurer struct {
-	healthCheck *mesh_core.HealthCheckResource
-}
+var _ ClusterConfigurer = &HealthCheckConfigurer{}
 
-func (e *healthCheckConfigurer) Configure(cluster *envoy_api.Cluster) error {
-	if e.healthCheck == nil || e.healthCheck.Spec.Conf == nil {
+func (e *HealthCheckConfigurer) Configure(cluster *envoy_api.Cluster) error {
+	if e.HealthCheck == nil || e.HealthCheck.Spec.Conf == nil {
 		return nil
 	}
-	activeChecks := e.healthCheck.Spec.Conf
+	activeChecks := e.HealthCheck.Spec.Conf
 	cluster.HealthChecks = append(cluster.HealthChecks, &envoy_core.HealthCheck{
 		HealthChecker: &envoy_core.HealthCheck_TcpHealthCheck_{
 			TcpHealthCheck: &envoy_core.HealthCheck_TcpHealthCheck{},
diff --git a/pkg/xds/envoy/clusters/v2/http2_configurer.go b/pkg/xds/envoy/clusters/v2/http2_configurer.go
new file mode 100644
index 000000000..bbebe6a3c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/http2_configurer.go
@@ -0,0 +1,16 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+)
+
+type Http2Configurer struct {
+}
+
+var _ ClusterConfigurer = &Http2Configurer{}
+
+func (p *Http2Configurer) Configure(c *envoy_api.Cluster) error {
+	c.Http2ProtocolOptions = &envoy_core.Http2ProtocolOptions{}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/lb_subset_configurer.go b/pkg/xds/envoy/clusters/v2/lb_subset_configurer.go
new file mode 100644
index 000000000..a8da6376b
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/lb_subset_configurer.go
@@ -0,0 +1,33 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+)
+
+type LbSubsetConfigurer struct {
+	KeySets [][]string
+}
+
+var _ ClusterConfigurer = &LbSubsetConfigurer{}
+
+func (e *LbSubsetConfigurer) Configure(c *envoy_api.Cluster) error {
+	var selectors []*envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector
+	for _, keys := range e.KeySets {
+		if len(keys) == 0 {
+			continue
+		}
+		selectors = append(selectors, &envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector{
+			Keys: keys,
+			// if there is a split by "version", and there is no endpoint with such version we should not fallback to all endpoints of the service
+			FallbackPolicy: envoy_api.Cluster_LbSubsetConfig_LbSubsetSelector_NO_FALLBACK,
+		})
+	}
+	if len(selectors) > 0 {
+		// if lb subset is set, but no label (Kuma's tag) is queried, we should return any endpoint
+		c.LbSubsetConfig = &envoy_api.Cluster_LbSubsetConfig{
+			FallbackPolicy:  envoy_api.Cluster_LbSubsetConfig_ANY_ENDPOINT,
+			SubsetSelectors: selectors,
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go
new file mode 100644
index 000000000..71d7311c5
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer.go
@@ -0,0 +1,18 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+)
+
+type PassThroughClusterConfigurer struct {
+	Name string
+}
+
+var _ ClusterConfigurer = &PassThroughClusterConfigurer{}
+
+func (p *PassThroughClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = p.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_ORIGINAL_DST}
+	c.LbPolicy = envoy_api.Cluster_CLUSTER_PROVIDED
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/static_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/static_cluster_configurer.go
new file mode 100644
index 000000000..dcabc6901
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/static_cluster_configurer.go
@@ -0,0 +1,22 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
+)
+
+type StaticClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &StaticClusterConfigurer{}
+
+func (e *StaticClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STATIC}
+	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go
new file mode 100644
index 000000000..7ab4ad6e0
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer.go
@@ -0,0 +1,24 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
+)
+
+type StrictDNSClusterConfigurer struct {
+	Name      string
+	Endpoints []xds.Endpoint
+}
+
+var _ ClusterConfigurer = &StrictDNSClusterConfigurer{}
+
+func (e *StrictDNSClusterConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_api.Cluster_Type{Type: envoy_api.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_api.Cluster_ROUND_ROBIN
+	c.LoadAssignment = envoy_endpoints.CreateClusterLoadAssignment(e.Name, e.Endpoints)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/timeout_configurer.go b/pkg/xds/envoy/clusters/v2/timeout_configurer.go
similarity index 57%
rename from pkg/xds/envoy/clusters/timeout_configurer.go
rename to pkg/xds/envoy/clusters/v2/timeout_configurer.go
index 7388ac1c9..8f4536ce0 100644
--- a/pkg/xds/envoy/clusters/timeout_configurer.go
+++ b/pkg/xds/envoy/clusters/v2/timeout_configurer.go
@@ -9,19 +9,13 @@ import (
 
 const defaultConnectTimeout = 5 * time.Second
 
-func ConnectTimeout(timeout time.Duration) ClusterBuilderOpt {
-	return ClusterBuilderOptFunc(func(config *ClusterBuilderConfig) {
-		config.Add(&timeoutConfigurer{
-			connectTimeout: timeout,
-		})
-	})
-}
-
-type timeoutConfigurer struct {
+type TimeoutConfigurer struct {
 	connectTimeout time.Duration
 }
 
-func (t *timeoutConfigurer) Configure(cluster *envoy_api.Cluster) error {
+var _ ClusterConfigurer = &TimeoutConfigurer{}
+
+func (t *TimeoutConfigurer) Configure(cluster *envoy_api.Cluster) error {
 	if t.connectTimeout.Nanoseconds() == 0 {
 		t.connectTimeout = defaultConnectTimeout
 	}
diff --git a/pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go b/pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go
new file mode 100644
index 000000000..e8b1a199c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/upstream_bind_config_configurer.go
@@ -0,0 +1,25 @@
+package clusters
+
+import (
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+)
+
+type UpstreamBindConfigConfigurer struct {
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &UpstreamBindConfigConfigurer{}
+
+func (u *UpstreamBindConfigConfigurer) Configure(c *envoy_api.Cluster) error {
+	c.UpstreamBindConfig = &envoy_core.BindConfig{
+		SourceAddress: &envoy_core.SocketAddress{
+			Address: u.Address,
+			PortSpecifier: &envoy_core.SocketAddress_PortValue{
+				PortValue: u.Port,
+			},
+		},
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go b/pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go
new file mode 100644
index 000000000..16b428c55
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/alt_stat_name_configurer.go
@@ -0,0 +1,20 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+)
+
+type AltStatNameConfigurer struct {
+}
+
+var _ ClusterConfigurer = &AltStatNameConfigurer{}
+
+func (e *AltStatNameConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	sanitizedName := util_xds.SanitizeMetric(cluster.Name)
+	if sanitizedName != cluster.Name {
+		cluster.AltStatName = sanitizedName
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go
new file mode 100644
index 000000000..9e1c57770
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer.go
@@ -0,0 +1,87 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+)
+
+type OutlierDetectionConfigurer struct {
+	CircuitBreaker *mesh_core.CircuitBreakerResource
+}
+
+var _ ClusterConfigurer = &OutlierDetectionConfigurer{}
+
+func (c *OutlierDetectionConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if c.CircuitBreaker == nil {
+		return nil
+	}
+
+	cluster.OutlierDetection = &envoy_cluster.OutlierDetection{
+		Interval:                       c.CircuitBreaker.Spec.GetConf().Interval,
+		BaseEjectionTime:               c.CircuitBreaker.Spec.GetConf().BaseEjectionTime,
+		MaxEjectionPercent:             c.CircuitBreaker.Spec.GetConf().MaxEjectionPercent,
+		SplitExternalLocalOriginErrors: c.CircuitBreaker.Spec.GetConf().SplitExternalAndLocalErrors,
+	}
+	c.configureTotalErrorDetector(cluster.OutlierDetection)
+	c.configureGatewayErrorDetector(cluster.OutlierDetection)
+	c.configureLocalErrorDetector(cluster.OutlierDetection)
+	c.configureStandardDeviationDetector(cluster.OutlierDetection)
+	c.configureFailureDetector(cluster.OutlierDetection)
+	return nil
+}
+
+func (c *OutlierDetectionConfigurer) configureTotalErrorDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if total := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetTotalErrors(); total != nil {
+		outlierDetection.Consecutive_5Xx = total.GetConsecutive()
+		outlierDetection.EnforcingConsecutive_5Xx = &wrappers.UInt32Value{Value: 100}
+	} else {
+		outlierDetection.EnforcingConsecutive_5Xx = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureGatewayErrorDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if gateway := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetGatewayErrors(); gateway != nil {
+		outlierDetection.ConsecutiveGatewayFailure = gateway.GetConsecutive()
+		outlierDetection.EnforcingConsecutiveGatewayFailure = &wrappers.UInt32Value{Value: 100}
+	} else {
+		outlierDetection.EnforcingConsecutiveGatewayFailure = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureLocalErrorDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if local := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetLocalErrors(); local != nil {
+		outlierDetection.ConsecutiveLocalOriginFailure = local.GetConsecutive()
+		outlierDetection.EnforcingConsecutiveLocalOriginFailure = &wrappers.UInt32Value{Value: 100}
+	} else {
+		outlierDetection.EnforcingConsecutiveLocalOriginFailure = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureStandardDeviationDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if stdev := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetStandardDeviation(); stdev != nil {
+		outlierDetection.SuccessRateRequestVolume = stdev.GetRequestVolume()
+		outlierDetection.SuccessRateMinimumHosts = stdev.GetMinimumHosts()
+		if factor := stdev.GetFactor(); factor != nil {
+			outlierDetection.SuccessRateStdevFactor = &wrappers.UInt32Value{Value: uint32(factor.GetValue() * 1000)}
+		}
+		outlierDetection.EnforcingSuccessRate = &wrappers.UInt32Value{Value: 100}
+		outlierDetection.EnforcingLocalOriginSuccessRate = &wrappers.UInt32Value{Value: 100} // takes effect only when SplitExternalLocalOriginErrors is true
+	} else {
+		outlierDetection.EnforcingSuccessRate = &wrappers.UInt32Value{Value: 0}
+	}
+}
+
+func (c *OutlierDetectionConfigurer) configureFailureDetector(outlierDetection *envoy_cluster.OutlierDetection) {
+	if failure := c.CircuitBreaker.Spec.GetConf().GetDetectors().GetFailure(); failure != nil {
+		outlierDetection.FailurePercentageRequestVolume = failure.GetRequestVolume()
+		outlierDetection.FailurePercentageMinimumHosts = failure.GetMinimumHosts()
+		outlierDetection.FailurePercentageThreshold = failure.GetThreshold()
+
+		outlierDetection.EnforcingFailurePercentage = &wrappers.UInt32Value{Value: 100}
+		outlierDetection.EnforcingFailurePercentageLocalOrigin = &wrappers.UInt32Value{Value: 100} // takes effect only when SplitExternalLocalOriginErrors is true
+	} else {
+		outlierDetection.EnforcingFailurePercentage = &wrappers.UInt32Value{Value: 0}
+	}
+}
diff --git a/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go
new file mode 100644
index 000000000..df2a8476a
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer.go
@@ -0,0 +1,84 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v3"
+	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls_v3 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+)
+
+type ClientSideMTLSConfigurer struct {
+	Ctx           xds_context.Context
+	Metadata      *core_xds.DataplaneMetadata
+	ClientService string
+	Tags          []envoy.Tags
+}
+
+var _ ClusterConfigurer = &ClientSideTLSConfigurer{}
+
+func (c *ClientSideMTLSConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if !c.Ctx.Mesh.Resource.MTLSEnabled() {
+		return nil
+	}
+	mesh := c.Ctx.Mesh.Resource.GetMeta().GetName()
+	// there might be a situation when there are multiple sam tags passed here for example two outbound listeners with the same tags, therefore we need to distinguish between them.
+	distinctTags := envoy.DistinctTags(c.Tags)
+	switch {
+	case len(distinctTags) == 0:
+		transportSocket, err := c.createTransportSocket("")
+		if err != nil {
+			return err
+		}
+		cluster.TransportSocket = transportSocket
+	case len(distinctTags) == 1:
+		transportSocket, err := c.createTransportSocket(tls.SNIFromTags(c.Tags[0].WithTags("mesh", mesh)))
+		if err != nil {
+			return err
+		}
+		cluster.TransportSocket = transportSocket
+	default:
+		for _, tags := range distinctTags {
+			sni := tls.SNIFromTags(tags.WithTags("mesh", mesh))
+			transportSocket, err := c.createTransportSocket(sni)
+			if err != nil {
+				return err
+			}
+			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_cluster.Cluster_TransportSocketMatch{
+				Name: sni,
+				Match: &pstruct.Struct{
+					Fields: envoy_metadata.MetadataFields(tags),
+				},
+				TransportSocket: transportSocket,
+			})
+		}
+	}
+	return nil
+}
+
+func (c *ClientSideMTLSConfigurer) createTransportSocket(sni string) (*envoy_core.TransportSocket, error) {
+	tlsContext, err := tls_v3.CreateUpstreamTlsContext(c.Ctx, c.Metadata, c.ClientService, sni)
+	if err != nil {
+		return nil, err
+	}
+	if tlsContext == nil {
+		return nil, nil
+	}
+	pbst, err := proto.MarshalAnyDeterministic(tlsContext)
+	if err != nil {
+		return nil, err
+	}
+	transportSocket := &envoy_core.TransportSocket{
+		Name: "envoy.transport_sockets.tls",
+		ConfigType: &envoy_core.TransportSocket_TypedConfig{
+			TypedConfig: pbst,
+		},
+	}
+	return transportSocket, nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go
new file mode 100644
index 000000000..1440111ce
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer.go
@@ -0,0 +1,56 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v3"
+	tls_v3 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type ClientSideTLSConfigurer struct {
+	Endpoints []xds.Endpoint
+}
+
+var _ ClusterConfigurer = &ClientSideTLSConfigurer{}
+
+func (c *ClientSideTLSConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	for _, ep := range c.Endpoints {
+		if ep.ExternalService.TLSEnabled {
+			tlsContext, err := tls_v3.UpstreamTlsContextOutsideMesh(
+				ep.ExternalService.CaCert,
+				ep.ExternalService.ClientCert,
+				ep.ExternalService.ClientKey,
+				ep.Target)
+			if err != nil {
+				return err
+			}
+
+			pbst, err := proto.MarshalAnyDeterministic(tlsContext)
+			if err != nil {
+				return err
+			}
+
+			transportSocket := &envoy_core.TransportSocket{
+				Name: "envoy.transport_sockets.tls",
+				ConfigType: &envoy_core.TransportSocket_TypedConfig{
+					TypedConfig: pbst,
+				},
+			}
+
+			cluster.TransportSocketMatches = append(cluster.TransportSocketMatches, &envoy_cluster.Cluster_TransportSocketMatch{
+				Name: ep.Target,
+				Match: &pstruct.Struct{
+					Fields: envoy_metadata.MetadataFields(ep.Tags),
+				},
+				TransportSocket: transportSocket,
+			})
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/configurer.go b/pkg/xds/envoy/clusters/v3/configurer.go
new file mode 100644
index 000000000..e148b5375
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/configurer.go
@@ -0,0 +1,12 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+)
+
+// ClusterConfigurer is responsible for configuring a single aspect of the entire Envoy cluster,
+// such as filter chain, transparent proxying, etc.
+type ClusterConfigurer interface {
+	// Configure configures a single aspect on a given Envoy cluster.
+	Configure(cluster *envoy_cluster.Cluster) error
+}
diff --git a/pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go
new file mode 100644
index 000000000..23efffa86
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer.go
@@ -0,0 +1,23 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+)
+
+type DnsClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &DnsClusterConfigurer{}
+
+func (e *DnsClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_cluster.Cluster_ROUND_ROBIN
+	c.LoadAssignment = endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
new file mode 100644
index 000000000..b5019132c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer.go
@@ -0,0 +1,25 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+)
+
+type EdsClusterConfigurer struct {
+	Name string
+}
+
+var _ ClusterConfigurer = &EdsClusterConfigurer{}
+
+func (e *EdsClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_EDS}
+	c.EdsClusterConfig = &envoy_cluster.Cluster_EdsClusterConfig{
+		EdsConfig: &envoy_core.ConfigSource{
+			ConfigSourceSpecifier: &envoy_core.ConfigSource_Ads{
+				Ads: &envoy_core.AggregatedConfigSource{},
+			},
+		},
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/health_check_configurer.go b/pkg/xds/envoy/clusters/v3/health_check_configurer.go
new file mode 100644
index 000000000..49f3933ef
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/health_check_configurer.go
@@ -0,0 +1,32 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+)
+
+type HealthCheckConfigurer struct {
+	HealthCheck *mesh_core.HealthCheckResource
+}
+
+var _ ClusterConfigurer = &HealthCheckConfigurer{}
+
+func (e *HealthCheckConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if e.HealthCheck == nil || e.HealthCheck.Spec.Conf == nil {
+		return nil
+	}
+	activeChecks := e.HealthCheck.Spec.Conf
+	cluster.HealthChecks = append(cluster.HealthChecks, &envoy_core.HealthCheck{
+		HealthChecker: &envoy_core.HealthCheck_TcpHealthCheck_{
+			TcpHealthCheck: &envoy_core.HealthCheck_TcpHealthCheck{},
+		},
+		Interval:           activeChecks.Interval,
+		Timeout:            activeChecks.Timeout,
+		UnhealthyThreshold: &wrappers.UInt32Value{Value: activeChecks.UnhealthyThreshold},
+		HealthyThreshold:   &wrappers.UInt32Value{Value: activeChecks.HealthyThreshold},
+	})
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/http2_configurer.go b/pkg/xds/envoy/clusters/v3/http2_configurer.go
new file mode 100644
index 000000000..84cff553d
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/http2_configurer.go
@@ -0,0 +1,36 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_upstream_http "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
+	"github.com/golang/protobuf/ptypes/any"
+
+	"github.com/kumahq/kuma/pkg/util/proto"
+)
+
+type Http2Configurer struct {
+}
+
+var _ ClusterConfigurer = &Http2Configurer{}
+
+func (p *Http2Configurer) Configure(c *envoy_cluster.Cluster) error {
+	options := &envoy_upstream_http.HttpProtocolOptions{
+		UpstreamProtocolOptions: &envoy_upstream_http.HttpProtocolOptions_ExplicitHttpConfig_{
+			ExplicitHttpConfig: &envoy_upstream_http.HttpProtocolOptions_ExplicitHttpConfig{
+				ProtocolConfig: &envoy_upstream_http.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{
+					Http2ProtocolOptions: &envoy_core.Http2ProtocolOptions{},
+				},
+			},
+		},
+	}
+
+	pbst, err := proto.MarshalAnyDeterministic(options)
+	if err != nil {
+		return err
+	}
+	c.TypedExtensionProtocolOptions = map[string]*any.Any{
+		"envoy.extensions.upstreams.http.v3.HttpProtocolOptions": pbst,
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/lb_subset_configurer.go b/pkg/xds/envoy/clusters/v3/lb_subset_configurer.go
new file mode 100644
index 000000000..508dae5dc
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/lb_subset_configurer.go
@@ -0,0 +1,33 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+)
+
+type LbSubsetConfigurer struct {
+	KeySets [][]string
+}
+
+var _ ClusterConfigurer = &LbSubsetConfigurer{}
+
+func (e *LbSubsetConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	var selectors []*envoy_cluster.Cluster_LbSubsetConfig_LbSubsetSelector
+	for _, keys := range e.KeySets {
+		if len(keys) == 0 {
+			continue
+		}
+		selectors = append(selectors, &envoy_cluster.Cluster_LbSubsetConfig_LbSubsetSelector{
+			Keys: keys,
+			// if there is a split by "version", and there is no endpoint with such version we should not fallback to all endpoints of the service
+			FallbackPolicy: envoy_cluster.Cluster_LbSubsetConfig_LbSubsetSelector_NO_FALLBACK,
+		})
+	}
+	if len(selectors) > 0 {
+		// if lb subset is set, but no label (Kuma's tag) is queried, we should return any endpoint
+		c.LbSubsetConfig = &envoy_cluster.Cluster_LbSubsetConfig{
+			FallbackPolicy:  envoy_cluster.Cluster_LbSubsetConfig_ANY_ENDPOINT,
+			SubsetSelectors: selectors,
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go
new file mode 100644
index 000000000..b5415e650
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer.go
@@ -0,0 +1,18 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+)
+
+type PassThroughClusterConfigurer struct {
+	Name string
+}
+
+var _ ClusterConfigurer = &PassThroughClusterConfigurer{}
+
+func (p *PassThroughClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = p.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_ORIGINAL_DST}
+	c.LbPolicy = envoy_cluster.Cluster_CLUSTER_PROVIDED
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/static_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/static_cluster_configurer.go
new file mode 100644
index 000000000..7729b38ec
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/static_cluster_configurer.go
@@ -0,0 +1,22 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+)
+
+type StaticClusterConfigurer struct {
+	Name    string
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &StaticClusterConfigurer{}
+
+func (e *StaticClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_STATIC}
+	c.LoadAssignment = envoy_endpoints.CreateStaticEndpoint(e.Name, e.Address, e.Port)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go
new file mode 100644
index 000000000..e7f877530
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer.go
@@ -0,0 +1,23 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+)
+
+type StrictDNSClusterConfigurer struct {
+	Name      string
+	Endpoints []xds.Endpoint
+}
+
+var _ ClusterConfigurer = &StrictDNSClusterConfigurer{}
+
+func (e *StrictDNSClusterConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.Name = e.Name
+	c.ClusterDiscoveryType = &envoy_cluster.Cluster_Type{Type: envoy_cluster.Cluster_STRICT_DNS}
+	c.LbPolicy = envoy_cluster.Cluster_ROUND_ROBIN
+	c.LoadAssignment = envoy_endpoints.CreateClusterLoadAssignment(e.Name, e.Endpoints)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/timeout_configurer.go b/pkg/xds/envoy/clusters/v3/timeout_configurer.go
new file mode 100644
index 000000000..690b28c80
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/timeout_configurer.go
@@ -0,0 +1,24 @@
+package clusters
+
+import (
+	"time"
+
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	"github.com/golang/protobuf/ptypes"
+)
+
+const defaultConnectTimeout = 5 * time.Second
+
+type TimeoutConfigurer struct {
+	connectTimeout time.Duration
+}
+
+var _ ClusterConfigurer = &TimeoutConfigurer{}
+
+func (t *TimeoutConfigurer) Configure(cluster *envoy_cluster.Cluster) error {
+	if t.connectTimeout.Nanoseconds() == 0 {
+		t.connectTimeout = defaultConnectTimeout
+	}
+	cluster.ConnectTimeout = ptypes.DurationProto(t.connectTimeout)
+	return nil
+}
diff --git a/pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go b/pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go
new file mode 100644
index 000000000..5bb388695
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/upstream_bind_config_configurer.go
@@ -0,0 +1,25 @@
+package clusters
+
+import (
+	envoy_cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+)
+
+type UpstreamBindConfigConfigurer struct {
+	Address string
+	Port    uint32
+}
+
+var _ ClusterConfigurer = &UpstreamBindConfigConfigurer{}
+
+func (u *UpstreamBindConfigConfigurer) Configure(c *envoy_cluster.Cluster) error {
+	c.UpstreamBindConfig = &envoy_core.BindConfig{
+		SourceAddress: &envoy_core.SocketAddress{
+			Address: u.Address,
+			PortSpecifier: &envoy_core.SocketAddress_PortValue{
+				PortValue: u.Port,
+			},
+		},
+	}
+	return nil
+}
diff --git a/pkg/xds/envoy/endpoints/endpoints.go b/pkg/xds/envoy/endpoints/v2/endpoints.go
similarity index 97%
rename from pkg/xds/envoy/endpoints/endpoints.go
rename to pkg/xds/envoy/endpoints/v2/endpoints.go
index 5f9652c96..1e5341f34 100644
--- a/pkg/xds/envoy/endpoints/endpoints.go
+++ b/pkg/xds/envoy/endpoints/v2/endpoints.go
@@ -9,7 +9,7 @@ import (
 	proto_wrappers "github.com/golang/protobuf/ptypes/wrappers"
 
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
-	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 )
 
 func CreateStaticEndpoint(clusterName string, address string, port uint32) *envoy_api.ClusterLoadAssignment {
@@ -46,7 +46,7 @@ func CreateClusterLoadAssignment(clusterName string, endpoints []core_xds.Endpoi
 			LoadBalancingWeight: &proto_wrappers.UInt32Value{
 				Value: ep.Weight,
 			},
-			Metadata: envoy_common.EndpointMetadata(ep.Tags),
+			Metadata: envoy_metadata.EndpointMetadata(ep.Tags),
 			HostIdentifier: &envoy_endpoint.LbEndpoint_Endpoint{
 				Endpoint: &envoy_endpoint.Endpoint{
 					Address: &envoy_core.Address{
diff --git a/pkg/xds/envoy/endpoints/v3/endpoints.go b/pkg/xds/envoy/endpoints/v3/endpoints.go
new file mode 100644
index 000000000..32dd49d91
--- /dev/null
+++ b/pkg/xds/envoy/endpoints/v3/endpoints.go
@@ -0,0 +1,130 @@
+package endpoints
+
+import (
+	"sort"
+
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
+	proto_wrappers "github.com/golang/protobuf/ptypes/wrappers"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	envoy "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v3"
+)
+
+func CreateStaticEndpoint(clusterName string, address string, port uint32) *envoy_endpoint.ClusterLoadAssignment {
+	return &envoy_endpoint.ClusterLoadAssignment{
+		ClusterName: clusterName,
+		Endpoints: []*envoy_endpoint.LocalityLbEndpoints{{
+			LbEndpoints: []*envoy_endpoint.LbEndpoint{{
+				HostIdentifier: &envoy_endpoint.LbEndpoint_Endpoint{
+					Endpoint: &envoy_endpoint.Endpoint{
+						Address: &envoy_core.Address{
+							Address: &envoy_core.Address_SocketAddress{
+								SocketAddress: &envoy_core.SocketAddress{
+									Protocol: envoy_core.SocketAddress_TCP,
+									Address:  address,
+									PortSpecifier: &envoy_core.SocketAddress_PortValue{
+										PortValue: port,
+									},
+								},
+							},
+						},
+					},
+				},
+			}},
+		}},
+	}
+}
+
+func CreateClusterLoadAssignment(clusterName string, endpoints []core_xds.Endpoint) *envoy_endpoint.ClusterLoadAssignment {
+	localityLbEndpoints := LocalityLbEndpointsMap{}
+
+	for _, ep := range endpoints {
+		lbEndpoints := localityLbEndpoints.Get(ep)
+		lbEndpoints.LbEndpoints = append(lbEndpoints.LbEndpoints, &envoy_endpoint.LbEndpoint{
+			LoadBalancingWeight: &proto_wrappers.UInt32Value{
+				Value: ep.Weight,
+			},
+			Metadata: envoy.EndpointMetadata(ep.Tags),
+			HostIdentifier: &envoy_endpoint.LbEndpoint_Endpoint{
+				Endpoint: &envoy_endpoint.Endpoint{
+					Address: &envoy_core.Address{
+						Address: &envoy_core.Address_SocketAddress{
+							SocketAddress: &envoy_core.SocketAddress{
+								Protocol: envoy_core.SocketAddress_TCP,
+								Address:  ep.Target,
+								PortSpecifier: &envoy_core.SocketAddress_PortValue{
+									PortValue: ep.Port,
+								},
+							},
+						},
+					},
+				}},
+		})
+	}
+
+	for _, lbEndpoints := range localityLbEndpoints {
+		// sort the slice to ensure stable Envoy configuration
+		sortLbEndpoints(lbEndpoints.LbEndpoints)
+	}
+
+	return &envoy_endpoint.ClusterLoadAssignment{
+		ClusterName: clusterName,
+		Endpoints:   localityLbEndpoints.AsSlice(),
+	}
+}
+
+func sortLbEndpoints(lbEndpoints []*envoy_endpoint.LbEndpoint) {
+	sort.Slice(lbEndpoints, func(i, j int) bool {
+		left, right := lbEndpoints[i], lbEndpoints[j]
+		leftAddr := left.GetEndpoint().GetAddress().GetSocketAddress().GetAddress()
+		rightAddr := right.GetEndpoint().GetAddress().GetSocketAddress().GetAddress()
+		if leftAddr == rightAddr {
+			return left.GetEndpoint().GetAddress().GetSocketAddress().GetPortValue() < right.GetEndpoint().GetAddress().GetSocketAddress().GetPortValue()
+		}
+		return leftAddr < rightAddr
+	})
+}
+
+type LocalityLbEndpointsMap map[string]*envoy_endpoint.LocalityLbEndpoints
+
+func (l LocalityLbEndpointsMap) Get(ep core_xds.Endpoint) *envoy_endpoint.LocalityLbEndpoints {
+	key := ep.LocalityString()
+	if _, ok := l[key]; !ok {
+		var locality *envoy_core.Locality
+		priority := uint32(0)
+		if ep.HasLocality() {
+			locality = &envoy_core.Locality{
+				Region:  ep.Locality.Region,
+				Zone:    ep.Locality.Zone,
+				SubZone: ep.Locality.SubZone,
+			}
+			priority = ep.Locality.Priority
+		}
+
+		l[key] = &envoy_endpoint.LocalityLbEndpoints{
+			LbEndpoints: make([]*envoy_endpoint.LbEndpoint, 0),
+			Locality:    locality,
+			Priority:    priority,
+		}
+	}
+
+	return l[key]
+}
+
+func (l LocalityLbEndpointsMap) AsSlice() []*envoy_endpoint.LocalityLbEndpoints {
+	slice := make([]*envoy_endpoint.LocalityLbEndpoints, 0)
+
+	for _, lle := range l {
+		slice = append(slice, lle)
+	}
+
+	// sort the slice to ensure stable Envoy configuration
+	sort.Slice(slice, func(i, j int) bool {
+		left, right := slice[i], slice[j]
+		return (left.Locality.Region + left.Locality.Zone + left.Locality.SubZone) >
+			(right.Locality.Region + right.Locality.Zone + right.Locality.SubZone)
+	})
+
+	return slice
+}
diff --git a/pkg/xds/envoy/listeners/network_rbac_configurer.go b/pkg/xds/envoy/listeners/network_rbac_configurer.go
index b00ff80f3..73f1f1202 100644
--- a/pkg/xds/envoy/listeners/network_rbac_configurer.go
+++ b/pkg/xds/envoy/listeners/network_rbac_configurer.go
@@ -3,7 +3,7 @@ package listeners
 import (
 	"fmt"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 
 	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
 	rbac "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/rbac/v2"
diff --git a/pkg/xds/envoy/listeners/server_mtls_configurer.go b/pkg/xds/envoy/listeners/server_mtls_configurer.go
index 3644c63b9..9cc803bef 100644
--- a/pkg/xds/envoy/listeners/server_mtls_configurer.go
+++ b/pkg/xds/envoy/listeners/server_mtls_configurer.go
@@ -4,7 +4,7 @@ import (
 	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
 	envoy_listener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+	tls "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/util/proto"
diff --git a/pkg/xds/envoy/listeners/tcp_proxy_configurer.go b/pkg/xds/envoy/listeners/tcp_proxy_configurer.go
index 97658bf3a..86896f6f2 100644
--- a/pkg/xds/envoy/listeners/tcp_proxy_configurer.go
+++ b/pkg/xds/envoy/listeners/tcp_proxy_configurer.go
@@ -7,6 +7,7 @@ import (
 	"github.com/kumahq/kuma/pkg/util/proto"
 	util_xds "github.com/kumahq/kuma/pkg/util/xds"
 	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 )
 
 func TcpProxy(statsName string, clusters ...envoy_common.ClusterSubset) FilterChainBuilderOpt {
@@ -52,14 +53,14 @@ func (c *TcpProxyConfigurer) tcpProxy() *envoy_tcp.TcpProxy {
 		proxy.ClusterSpecifier = &envoy_tcp.TcpProxy_Cluster{
 			Cluster: c.clusters[0].ClusterName,
 		}
-		proxy.MetadataMatch = envoy_common.LbMetadata(c.clusters[0].Tags)
+		proxy.MetadataMatch = envoy_metadata.LbMetadata(c.clusters[0].Tags)
 	} else {
 		var weightedClusters []*envoy_tcp.TcpProxy_WeightedCluster_ClusterWeight
 		for _, cluster := range c.clusters {
 			weightedClusters = append(weightedClusters, &envoy_tcp.TcpProxy_WeightedCluster_ClusterWeight{
 				Name:          cluster.ClusterName,
 				Weight:        cluster.Weight,
-				MetadataMatch: envoy_common.LbMetadata(cluster.Tags),
+				MetadataMatch: envoy_metadata.LbMetadata(cluster.Tags),
 			})
 		}
 		proxy.ClusterSpecifier = &envoy_tcp.TcpProxy_WeightedClusters{
diff --git a/pkg/xds/envoy/metadata.go b/pkg/xds/envoy/metadata/v2/metadata.go
similarity index 82%
rename from pkg/xds/envoy/metadata.go
rename to pkg/xds/envoy/metadata/v2/metadata.go
index 6c5856d8a..d40d92cc8 100644
--- a/pkg/xds/envoy/metadata.go
+++ b/pkg/xds/envoy/metadata/v2/metadata.go
@@ -5,9 +5,11 @@ import (
 	pstruct "github.com/golang/protobuf/ptypes/struct"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
 )
 
-func EndpointMetadata(tags Tags) *envoy_core.Metadata {
+func EndpointMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
 	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
 	if len(tags) == 0 {
 		return nil
@@ -26,7 +28,7 @@ func EndpointMetadata(tags Tags) *envoy_core.Metadata {
 	return metadata
 }
 
-func LbMetadata(tags Tags) *envoy_core.Metadata {
+func LbMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
 	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
 	if len(tags) == 0 {
 		return nil
@@ -42,7 +44,7 @@ func LbMetadata(tags Tags) *envoy_core.Metadata {
 	return metadata
 }
 
-func MetadataFields(tags Tags) map[string]*pstruct.Value {
+func MetadataFields(tags envoy_common.Tags) map[string]*pstruct.Value {
 	fields := map[string]*pstruct.Value{}
 	for key, value := range tags {
 		if key == mesh_proto.ServiceTag {
diff --git a/pkg/xds/envoy/metadata_test.go b/pkg/xds/envoy/metadata/v2/metadata_test.go
similarity index 100%
rename from pkg/xds/envoy/metadata_test.go
rename to pkg/xds/envoy/metadata/v2/metadata_test.go
diff --git a/pkg/xds/envoy/metadata/v3/metadata.go b/pkg/xds/envoy/metadata/v3/metadata.go
new file mode 100644
index 000000000..82408a2e9
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v3/metadata.go
@@ -0,0 +1,60 @@
+package envoy
+
+import (
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+
+	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+)
+
+func EndpointMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
+	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
+	if len(tags) == 0 {
+		return nil
+	}
+	fields := MetadataFields(tags)
+	metadata := &envoy_core.Metadata{
+		FilterMetadata: map[string]*pstruct.Struct{
+			"envoy.lb": {
+				Fields: fields,
+			},
+			"envoy.transport_socket_match": {
+				Fields: fields,
+			},
+		},
+	}
+	return metadata
+}
+
+func LbMetadata(tags envoy_common.Tags) *envoy_core.Metadata {
+	tags = tags.WithoutTag(mesh_proto.ServiceTag) // service name is already in cluster name, we don't need it in metadata
+	if len(tags) == 0 {
+		return nil
+	}
+	fields := MetadataFields(tags)
+	metadata := &envoy_core.Metadata{
+		FilterMetadata: map[string]*pstruct.Struct{
+			"envoy.lb": {
+				Fields: fields,
+			},
+		},
+	}
+	return metadata
+}
+
+func MetadataFields(tags envoy_common.Tags) map[string]*pstruct.Value {
+	fields := map[string]*pstruct.Value{}
+	for key, value := range tags {
+		if key == mesh_proto.ServiceTag {
+			continue
+		}
+		fields[key] = &pstruct.Value{
+			Kind: &pstruct.Value_StringValue{
+				StringValue: value,
+			},
+		}
+	}
+	return fields
+}
diff --git a/pkg/xds/envoy/routes/default_route_configurer.go b/pkg/xds/envoy/routes/default_route_configurer.go
index 017d129fe..744d6c72c 100644
--- a/pkg/xds/envoy/routes/default_route_configurer.go
+++ b/pkg/xds/envoy/routes/default_route_configurer.go
@@ -7,6 +7,7 @@ import (
 	envoy_route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
 
 	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 )
 
 func DefaultRoute(subsets ...envoy_common.ClusterSubset) VirtualHostBuilderOpt {
@@ -53,7 +54,7 @@ func (c RouteConfigurer) routeAction() *envoy_route.RouteAction {
 		routeAction.ClusterSpecifier = &envoy_route.RouteAction_Cluster{
 			Cluster: c.subsets[0].ClusterName,
 		}
-		routeAction.MetadataMatch = envoy_common.LbMetadata(c.subsets[0].Tags)
+		routeAction.MetadataMatch = envoy_metadata.LbMetadata(c.subsets[0].Tags)
 	} else {
 		var weightedClusters []*envoy_route.WeightedCluster_ClusterWeight
 		var totalWeight uint32
@@ -61,7 +62,7 @@ func (c RouteConfigurer) routeAction() *envoy_route.RouteAction {
 			weightedClusters = append(weightedClusters, &envoy_route.WeightedCluster_ClusterWeight{
 				Name:          subset.ClusterName,
 				Weight:        &wrappers.UInt32Value{Value: subset.Weight},
-				MetadataMatch: envoy_common.LbMetadata(subset.Tags),
+				MetadataMatch: envoy_metadata.LbMetadata(subset.Tags),
 			})
 			totalWeight += subset.Weight
 		}
diff --git a/pkg/xds/envoy/tls/tls.go b/pkg/xds/envoy/tls/tls.go
index 53b65c4bb..a42601355 100644
--- a/pkg/xds/envoy/tls/tls.go
+++ b/pkg/xds/envoy/tls/tls.go
@@ -2,17 +2,6 @@ package tls
 
 import (
 	"fmt"
-
-	envoy_auth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-	envoy_grpc_credential "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v2alpha"
-	envoy_type_matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
-	"github.com/golang/protobuf/ptypes/wrappers"
-
-	core_xds "github.com/kumahq/kuma/pkg/core/xds"
-	"github.com/kumahq/kuma/pkg/util/proto"
-	util_xds "github.com/kumahq/kuma/pkg/util/xds"
-	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 )
 
 const (
@@ -20,200 +9,10 @@ const (
 	IdentityCertResource = "identity_cert"
 )
 
-// CreateDownstreamTlsContext creates DownstreamTlsContext for incoming connections
-// It verifies that incoming connection has TLS certificate signed by Mesh CA with URI SAN of prefix spiffe://{mesh_name}/
-// It secures inbound listener with certificate of "identity_cert" that will be received from the SDS (it contains URI SANs of all inbounds).
-// Access to SDS is secured by TLS certificate (set in config or autogenerated at CP start) and path to dataplane token
-func CreateDownstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) (*envoy_auth.DownstreamTlsContext, error) {
-	if !ctx.Mesh.Resource.MTLSEnabled() {
-		return nil, nil
-	}
-	validationSANMatcher := MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
-	commonTlsContext, err := CreateCommonTlsContext(ctx, metadata, validationSANMatcher)
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.DownstreamTlsContext{
-		CommonTlsContext:         commonTlsContext,
-		RequireClientCertificate: &wrappers.BoolValue{Value: true},
-	}, nil
-}
-
-// CreateUpstreamTlsContext creates UpstreamTlsContext for outgoing connections
-// It verifies that the upstream server has TLS certificate signed by Mesh CA with URI SAN of spiffe://{mesh_name}/{upstream_service}
-// The downstream client exposes for the upstream server cert with multiple URI SANs, which means that if DP has inbound with services "web" and "web-api" and communicates with "backend"
-// the upstream server ("backend") will see that DP with TLS certificate of URIs of "web" and "web-api".
-// There is no way to correlate incoming request to "web" or "web-api" with outgoing request to "backend" to expose only one URI SAN.
-//
-// Pass "*" for upstreamService to validate that upstream service is a service that is part of the mesh (but not specific one)
-func CreateUpstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, upstreamService string, sni string) (*envoy_auth.UpstreamTlsContext, error) {
-	if !ctx.Mesh.Resource.MTLSEnabled() {
-		return nil, nil
-	}
-	var validationSANMatcher *envoy_type_matcher.StringMatcher
-	if upstreamService == "*" {
-		validationSANMatcher = MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
-	} else {
-		validationSANMatcher = ServiceSpiffeIDMatcher(ctx.Mesh.Resource.Meta.GetName(), upstreamService)
-	}
-	commonTlsContext, err := CreateCommonTlsContext(ctx, metadata, validationSANMatcher)
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.UpstreamTlsContext{
-		CommonTlsContext: commonTlsContext,
-		Sni:              sni,
-	}, nil
-}
-
-func CreateCommonTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, validationSANMatcher *envoy_type_matcher.StringMatcher) (*envoy_auth.CommonTlsContext, error) {
-	meshCaSecret, err := sdsSecretConfig(ctx, MeshCaResource, metadata)
-	if err != nil {
-		return nil, err
-	}
-	identitySecret, err := sdsSecretConfig(ctx, IdentityCertResource, metadata)
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.CommonTlsContext{
-		ValidationContextType: &envoy_auth.CommonTlsContext_CombinedValidationContext{
-			CombinedValidationContext: &envoy_auth.CommonTlsContext_CombinedCertificateValidationContext{
-				DefaultValidationContext: &envoy_auth.CertificateValidationContext{
-					MatchSubjectAltNames: []*envoy_type_matcher.StringMatcher{validationSANMatcher},
-				},
-				ValidationContextSdsSecretConfig: meshCaSecret,
-			},
-		},
-		TlsCertificateSdsSecretConfigs: []*envoy_auth.SdsSecretConfig{
-			identitySecret,
-		},
-	}, nil
-}
-
-func sdsSecretConfig(context xds_context.Context, name string, metadata *core_xds.DataplaneMetadata) (*envoy_auth.SdsSecretConfig, error) {
-	withCallCredentials := func(grpc *envoy_core.GrpcService_GoogleGrpc) (*envoy_core.GrpcService_GoogleGrpc, error) {
-		if metadata.GetDataplaneTokenPath() == "" {
-			return grpc, nil
-		}
-
-		config := &envoy_grpc_credential.FileBasedMetadataConfig{
-			SecretData: &envoy_core.DataSource{
-				Specifier: &envoy_core.DataSource_Filename{
-					Filename: metadata.GetDataplaneTokenPath(),
-				},
-			},
-		}
-		typedConfig, err := proto.MarshalAnyDeterministic(config)
-		if err != nil {
-			return nil, err
-		}
-
-		grpc.CallCredentials = append(grpc.CallCredentials, &envoy_core.GrpcService_GoogleGrpc_CallCredentials{
-			CredentialSpecifier: &envoy_core.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
-				FromPlugin: &envoy_core.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
-					Name: "envoy.grpc_credentials.file_based_metadata",
-					ConfigType: &envoy_core.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
-						TypedConfig: typedConfig,
-					},
-				},
-			},
-		})
-		grpc.CredentialsFactoryName = "envoy.grpc_credentials.file_based_metadata"
-
-		return grpc, nil
-	}
-	googleGrpc, err := withCallCredentials(&envoy_core.GrpcService_GoogleGrpc{
-		TargetUri:  context.SDSLocation(),
-		StatPrefix: util_xds.SanitizeMetric("sds_" + name),
-		ChannelCredentials: &envoy_core.GrpcService_GoogleGrpc_ChannelCredentials{
-			CredentialSpecifier: &envoy_core.GrpcService_GoogleGrpc_ChannelCredentials_SslCredentials{
-				SslCredentials: &envoy_core.GrpcService_GoogleGrpc_SslCredentials{
-					RootCerts: &envoy_core.DataSource{
-						Specifier: &envoy_core.DataSource_InlineBytes{
-							InlineBytes: context.ControlPlane.SdsTlsCert,
-						},
-					},
-				},
-			},
-		},
-	})
-	if err != nil {
-		return nil, err
-	}
-	return &envoy_auth.SdsSecretConfig{
-		Name: name,
-		SdsConfig: &envoy_core.ConfigSource{
-			ConfigSourceSpecifier: &envoy_core.ConfigSource_ApiConfigSource{
-				ApiConfigSource: &envoy_core.ApiConfigSource{
-					ApiType: envoy_core.ApiConfigSource_GRPC,
-					GrpcServices: []*envoy_core.GrpcService{
-						{
-							TargetSpecifier: &envoy_core.GrpcService_GoogleGrpc_{
-								GoogleGrpc: googleGrpc,
-							},
-						},
-					},
-				},
-			},
-		},
-	}, nil
-}
-
 func MeshSpiffeIDPrefix(mesh string) string {
 	return fmt.Sprintf("spiffe://%s/", mesh)
 }
 
-func MeshSpiffeIDPrefixMatcher(mesh string) *envoy_type_matcher.StringMatcher {
-	return &envoy_type_matcher.StringMatcher{
-		MatchPattern: &envoy_type_matcher.StringMatcher_Prefix{
-			Prefix: MeshSpiffeIDPrefix(mesh),
-		},
-	}
-}
-
 func ServiceSpiffeID(mesh string, service string) string {
 	return fmt.Sprintf("spiffe://%s/%s", mesh, service)
 }
-
-func ServiceSpiffeIDMatcher(mesh string, service string) *envoy_type_matcher.StringMatcher {
-	return &envoy_type_matcher.StringMatcher{
-		MatchPattern: &envoy_type_matcher.StringMatcher_Exact{
-			Exact: ServiceSpiffeID(mesh, service),
-		},
-	}
-}
-
-func UpstreamTlsContextOutsideMesh(ca, cert, key *envoy_core.DataSource, hostname string) (*envoy_auth.UpstreamTlsContext, error) {
-	var tlsCertificates []*envoy_auth.TlsCertificate
-	if cert != nil && key != nil {
-		tlsCertificates = []*envoy_auth.TlsCertificate{
-			{
-				CertificateChain: cert,
-				PrivateKey:       key,
-			},
-		}
-	}
-
-	var validationContextType *envoy_auth.CommonTlsContext_ValidationContext
-	if ca != nil {
-		validationContextType = &envoy_auth.CommonTlsContext_ValidationContext{
-			ValidationContext: &envoy_auth.CertificateValidationContext{
-				TrustedCa: ca,
-				MatchSubjectAltNames: []*envoy_type_matcher.StringMatcher{
-					{
-						MatchPattern: &envoy_type_matcher.StringMatcher_Exact{
-							Exact: hostname,
-						},
-					},
-				},
-			},
-		}
-	}
-
-	return &envoy_auth.UpstreamTlsContext{
-		CommonTlsContext: &envoy_auth.CommonTlsContext{
-			TlsCertificates:       tlsCertificates,
-			ValidationContextType: validationContextType,
-		},
-	}, nil
-}
diff --git a/pkg/xds/envoy/tls/v2/tls.go b/pkg/xds/envoy/tls/v2/tls.go
new file mode 100644
index 000000000..c67366676
--- /dev/null
+++ b/pkg/xds/envoy/tls/v2/tls.go
@@ -0,0 +1,213 @@
+package v2
+
+import (
+	envoy_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
+	envoy_core_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_grpc_credential_v2 "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v2alpha"
+	envoy_type_matcher_v2 "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+)
+
+// CreateDownstreamTlsContext creates DownstreamTlsContext for incoming connections
+// It verifies that incoming connection has TLS certificate signed by Mesh CA with URI SAN of prefix spiffe://{mesh_name}/
+// It secures inbound listener with certificate of "identity_cert" that will be received from the SDS (it contains URI SANs of all inbounds).
+// Access to SDS is secured by TLS certificate (set in config or autogenerated at CP start) and path to dataplane token
+func CreateDownstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) (*envoy_auth_v2.DownstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	validationSANMatcher := MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.DownstreamTlsContext{
+		CommonTlsContext:         commonTlsContext,
+		RequireClientCertificate: &wrappers.BoolValue{Value: true},
+	}, nil
+}
+
+// CreateUpstreamTlsContext creates UpstreamTlsContext for outgoing connections
+// It verifies that the upstream server has TLS certificate signed by Mesh CA with URI SAN of spiffe://{mesh_name}/{upstream_service}
+// The downstream client exposes for the upstream server cert with multiple URI SANs, which means that if DP has inbound with services "web" and "web-api" and communicates with "backend"
+// the upstream server ("backend") will see that DP with TLS certificate of URIs of "web" and "web-api".
+// There is no way to correlate incoming request to "web" or "web-api" with outgoing request to "backend" to expose only one URI SAN.
+//
+// Pass "*" for upstreamService to validate that upstream service is a service that is part of the mesh (but not specific one)
+func CreateUpstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, upstreamService string, sni string) (*envoy_auth_v2.UpstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	var validationSANMatcher *envoy_type_matcher_v2.StringMatcher
+	if upstreamService == "*" {
+		validationSANMatcher = MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	} else {
+		validationSANMatcher = ServiceSpiffeIDMatcher(ctx.Mesh.Resource.Meta.GetName(), upstreamService)
+	}
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.UpstreamTlsContext{
+		CommonTlsContext: commonTlsContext,
+		Sni:              sni,
+	}, nil
+}
+
+func createCommonTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, validationSANMatcher *envoy_type_matcher_v2.StringMatcher) (*envoy_auth_v2.CommonTlsContext, error) {
+	meshCaSecret, err := sdsSecretConfig(ctx, tls.MeshCaResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	identitySecret, err := sdsSecretConfig(ctx, tls.IdentityCertResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.CommonTlsContext{
+		ValidationContextType: &envoy_auth_v2.CommonTlsContext_CombinedValidationContext{
+			CombinedValidationContext: &envoy_auth_v2.CommonTlsContext_CombinedCertificateValidationContext{
+				DefaultValidationContext: &envoy_auth_v2.CertificateValidationContext{
+					MatchSubjectAltNames: []*envoy_type_matcher_v2.StringMatcher{validationSANMatcher},
+				},
+				ValidationContextSdsSecretConfig: meshCaSecret,
+			},
+		},
+		TlsCertificateSdsSecretConfigs: []*envoy_auth_v2.SdsSecretConfig{
+			identitySecret,
+		},
+	}, nil
+}
+
+func sdsSecretConfig(context xds_context.Context, name string, metadata *core_xds.DataplaneMetadata) (*envoy_auth_v2.SdsSecretConfig, error) {
+	withCallCredentials := func(grpc *envoy_core_v2.GrpcService_GoogleGrpc) (*envoy_core_v2.GrpcService_GoogleGrpc, error) {
+		if metadata.GetDataplaneTokenPath() == "" {
+			return grpc, nil
+		}
+
+		config := &envoy_grpc_credential_v2.FileBasedMetadataConfig{
+			SecretData: &envoy_core_v2.DataSource{
+				Specifier: &envoy_core_v2.DataSource_Filename{
+					Filename: metadata.GetDataplaneTokenPath(),
+				},
+			},
+		}
+		typedConfig, err := proto.MarshalAnyDeterministic(config)
+		if err != nil {
+			return nil, err
+		}
+
+		grpc.CallCredentials = append(grpc.CallCredentials, &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials{
+			CredentialSpecifier: &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
+				FromPlugin: &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
+					Name: "envoy.grpc_credentials.file_based_metadata",
+					ConfigType: &envoy_core_v2.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
+						TypedConfig: typedConfig,
+					},
+				},
+			},
+		})
+		grpc.CredentialsFactoryName = "envoy.grpc_credentials.file_based_metadata"
+
+		return grpc, nil
+	}
+	googleGrpc, err := withCallCredentials(&envoy_core_v2.GrpcService_GoogleGrpc{
+		TargetUri:  context.SDSLocation(),
+		StatPrefix: util_xds.SanitizeMetric("sds_" + name),
+		ChannelCredentials: &envoy_core_v2.GrpcService_GoogleGrpc_ChannelCredentials{
+			CredentialSpecifier: &envoy_core_v2.GrpcService_GoogleGrpc_ChannelCredentials_SslCredentials{
+				SslCredentials: &envoy_core_v2.GrpcService_GoogleGrpc_SslCredentials{
+					RootCerts: &envoy_core_v2.DataSource{
+						Specifier: &envoy_core_v2.DataSource_InlineBytes{
+							InlineBytes: context.ControlPlane.SdsTlsCert,
+						},
+					},
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_auth_v2.SdsSecretConfig{
+		Name: name,
+		SdsConfig: &envoy_core_v2.ConfigSource{
+			ConfigSourceSpecifier: &envoy_core_v2.ConfigSource_ApiConfigSource{
+				ApiConfigSource: &envoy_core_v2.ApiConfigSource{
+					ApiType: envoy_core_v2.ApiConfigSource_GRPC,
+					GrpcServices: []*envoy_core_v2.GrpcService{
+						{
+							TargetSpecifier: &envoy_core_v2.GrpcService_GoogleGrpc_{
+								GoogleGrpc: googleGrpc,
+							},
+						},
+					},
+				},
+			},
+		},
+	}, nil
+}
+
+func UpstreamTlsContextOutsideMesh(ca, cert, key []byte, hostname string) (*envoy_auth_v2.UpstreamTlsContext, error) {
+	var tlsCertificates []*envoy_auth_v2.TlsCertificate
+	if cert != nil && key != nil {
+		tlsCertificates = []*envoy_auth_v2.TlsCertificate{
+			{
+				CertificateChain: dataSourceFromBytes(cert),
+				PrivateKey:       dataSourceFromBytes(key),
+			},
+		}
+	}
+
+	var validationContextType *envoy_auth_v2.CommonTlsContext_ValidationContext
+	if ca != nil {
+		validationContextType = &envoy_auth_v2.CommonTlsContext_ValidationContext{
+			ValidationContext: &envoy_auth_v2.CertificateValidationContext{
+				TrustedCa: dataSourceFromBytes(ca),
+				MatchSubjectAltNames: []*envoy_type_matcher_v2.StringMatcher{
+					{
+						MatchPattern: &envoy_type_matcher_v2.StringMatcher_Exact{
+							Exact: hostname,
+						},
+					},
+				},
+			},
+		}
+	}
+
+	return &envoy_auth_v2.UpstreamTlsContext{
+		CommonTlsContext: &envoy_auth_v2.CommonTlsContext{
+			TlsCertificates:       tlsCertificates,
+			ValidationContextType: validationContextType,
+		},
+	}, nil
+}
+
+func dataSourceFromBytes(bytes []byte) *envoy_core_v2.DataSource {
+	return &envoy_core_v2.DataSource{
+		Specifier: &envoy_core_v2.DataSource_InlineBytes{
+			InlineBytes: bytes,
+		},
+	}
+}
+
+func MeshSpiffeIDPrefixMatcher(mesh string) *envoy_type_matcher_v2.StringMatcher {
+	return &envoy_type_matcher_v2.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v2.StringMatcher_Prefix{
+			Prefix: tls.MeshSpiffeIDPrefix(mesh),
+		},
+	}
+}
+
+func ServiceSpiffeIDMatcher(mesh string, service string) *envoy_type_matcher_v2.StringMatcher {
+	return &envoy_type_matcher_v2.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v2.StringMatcher_Exact{
+			Exact: tls.ServiceSpiffeID(mesh, service),
+		},
+	}
+}
diff --git a/pkg/xds/envoy/tls/v3/tls.go b/pkg/xds/envoy/tls/v3/tls.go
new file mode 100644
index 000000000..b3b839ece
--- /dev/null
+++ b/pkg/xds/envoy/tls/v3/tls.go
@@ -0,0 +1,214 @@
+package v3
+
+import (
+	envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
+	envoy_grpc_credential_v3 "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3"
+	envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
+	envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
+
+	"github.com/golang/protobuf/ptypes/wrappers"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/util/proto"
+	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
+)
+
+// CreateDownstreamTlsContext creates DownstreamTlsContext for incoming connections
+// It verifies that incoming connection has TLS certificate signed by Mesh CA with URI SAN of prefix spiffe://{mesh_name}/
+// It secures inbound listener with certificate of "identity_cert" that will be received from the SDS (it contains URI SANs of all inbounds).
+// Access to SDS is secured by TLS certificate (set in config or autogenerated at CP start) and path to dataplane token
+func CreateDownstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata) (*envoy_tls_v3.DownstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	validationSANMatcher := MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.DownstreamTlsContext{
+		CommonTlsContext:         commonTlsContext,
+		RequireClientCertificate: &wrappers.BoolValue{Value: true},
+	}, nil
+}
+
+// CreateUpstreamTlsContext creates UpstreamTlsContext for outgoing connections
+// It verifies that the upstream server has TLS certificate signed by Mesh CA with URI SAN of spiffe://{mesh_name}/{upstream_service}
+// The downstream client exposes for the upstream server cert with multiple URI SANs, which means that if DP has inbound with services "web" and "web-api" and communicates with "backend"
+// the upstream server ("backend") will see that DP with TLS certificate of URIs of "web" and "web-api".
+// There is no way to correlate incoming request to "web" or "web-api" with outgoing request to "backend" to expose only one URI SAN.
+//
+// Pass "*" for upstreamService to validate that upstream service is a service that is part of the mesh (but not specific one)
+func CreateUpstreamTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, upstreamService string, sni string) (*envoy_tls_v3.UpstreamTlsContext, error) {
+	if !ctx.Mesh.Resource.MTLSEnabled() {
+		return nil, nil
+	}
+	var validationSANMatcher *envoy_type_matcher_v3.StringMatcher
+	if upstreamService == "*" {
+		validationSANMatcher = MeshSpiffeIDPrefixMatcher(ctx.Mesh.Resource.Meta.GetName())
+	} else {
+		validationSANMatcher = ServiceSpiffeIDMatcher(ctx.Mesh.Resource.Meta.GetName(), upstreamService)
+	}
+	commonTlsContext, err := createCommonTlsContext(ctx, metadata, validationSANMatcher)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.UpstreamTlsContext{
+		CommonTlsContext: commonTlsContext,
+		Sni:              sni,
+	}, nil
+}
+
+func createCommonTlsContext(ctx xds_context.Context, metadata *core_xds.DataplaneMetadata, validationSANMatcher *envoy_type_matcher_v3.StringMatcher) (*envoy_tls_v3.CommonTlsContext, error) {
+	meshCaSecret, err := sdsSecretConfig(ctx, tls.MeshCaResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	identitySecret, err := sdsSecretConfig(ctx, tls.IdentityCertResource, metadata)
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.CommonTlsContext{
+		ValidationContextType: &envoy_tls_v3.CommonTlsContext_CombinedValidationContext{
+			CombinedValidationContext: &envoy_tls_v3.CommonTlsContext_CombinedCertificateValidationContext{
+				DefaultValidationContext: &envoy_tls_v3.CertificateValidationContext{
+					MatchSubjectAltNames: []*envoy_type_matcher_v3.StringMatcher{validationSANMatcher},
+				},
+				ValidationContextSdsSecretConfig: meshCaSecret,
+			},
+		},
+		TlsCertificateSdsSecretConfigs: []*envoy_tls_v3.SdsSecretConfig{
+			identitySecret,
+		},
+	}, nil
+}
+
+func sdsSecretConfig(context xds_context.Context, name string, metadata *core_xds.DataplaneMetadata) (*envoy_tls_v3.SdsSecretConfig, error) {
+	withCallCredentials := func(grpc *envoy_core_v3.GrpcService_GoogleGrpc) (*envoy_core_v3.GrpcService_GoogleGrpc, error) {
+		if metadata.GetDataplaneTokenPath() == "" {
+			return grpc, nil
+		}
+
+		config := &envoy_grpc_credential_v3.FileBasedMetadataConfig{
+			SecretData: &envoy_core_v3.DataSource{
+				Specifier: &envoy_core_v3.DataSource_Filename{
+					Filename: metadata.GetDataplaneTokenPath(),
+				},
+			},
+		}
+		typedConfig, err := proto.MarshalAnyDeterministic(config)
+		if err != nil {
+			return nil, err
+		}
+
+		grpc.CallCredentials = append(grpc.CallCredentials, &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials{
+			CredentialSpecifier: &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials_FromPlugin{
+				FromPlugin: &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin{
+					Name: "envoy.grpc_credentials.file_based_metadata",
+					ConfigType: &envoy_core_v3.GrpcService_GoogleGrpc_CallCredentials_MetadataCredentialsFromPlugin_TypedConfig{
+						TypedConfig: typedConfig,
+					},
+				},
+			},
+		})
+		grpc.CredentialsFactoryName = "envoy.grpc_credentials.file_based_metadata"
+
+		return grpc, nil
+	}
+	googleGrpc, err := withCallCredentials(&envoy_core_v3.GrpcService_GoogleGrpc{
+		TargetUri:  context.SDSLocation(),
+		StatPrefix: util_xds.SanitizeMetric("sds_" + name),
+		ChannelCredentials: &envoy_core_v3.GrpcService_GoogleGrpc_ChannelCredentials{
+			CredentialSpecifier: &envoy_core_v3.GrpcService_GoogleGrpc_ChannelCredentials_SslCredentials{
+				SslCredentials: &envoy_core_v3.GrpcService_GoogleGrpc_SslCredentials{
+					RootCerts: &envoy_core_v3.DataSource{
+						Specifier: &envoy_core_v3.DataSource_InlineBytes{
+							InlineBytes: context.ControlPlane.SdsTlsCert,
+						},
+					},
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &envoy_tls_v3.SdsSecretConfig{
+		Name: name,
+		SdsConfig: &envoy_core_v3.ConfigSource{
+			ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_ApiConfigSource{
+				ApiConfigSource: &envoy_core_v3.ApiConfigSource{
+					ApiType: envoy_core_v3.ApiConfigSource_GRPC,
+					GrpcServices: []*envoy_core_v3.GrpcService{
+						{
+							TargetSpecifier: &envoy_core_v3.GrpcService_GoogleGrpc_{
+								GoogleGrpc: googleGrpc,
+							},
+						},
+					},
+				},
+			},
+		},
+	}, nil
+}
+
+func UpstreamTlsContextOutsideMesh(ca, cert, key []byte, hostname string) (*envoy_tls_v3.UpstreamTlsContext, error) {
+	var tlsCertificates []*envoy_tls_v3.TlsCertificate
+	if cert != nil && key != nil {
+		tlsCertificates = []*envoy_tls_v3.TlsCertificate{
+			{
+				CertificateChain: dataSourceFromBytes(cert),
+				PrivateKey:       dataSourceFromBytes(key),
+			},
+		}
+	}
+
+	var validationContextType *envoy_tls_v3.CommonTlsContext_ValidationContext
+	if ca != nil {
+		validationContextType = &envoy_tls_v3.CommonTlsContext_ValidationContext{
+			ValidationContext: &envoy_tls_v3.CertificateValidationContext{
+				TrustedCa: dataSourceFromBytes(ca),
+				MatchSubjectAltNames: []*envoy_type_matcher_v3.StringMatcher{
+					{
+						MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
+							Exact: hostname,
+						},
+					},
+				},
+			},
+		}
+	}
+
+	return &envoy_tls_v3.UpstreamTlsContext{
+		CommonTlsContext: &envoy_tls_v3.CommonTlsContext{
+			TlsCertificates:       tlsCertificates,
+			ValidationContextType: validationContextType,
+		},
+	}, nil
+}
+
+func dataSourceFromBytes(bytes []byte) *envoy_core_v3.DataSource {
+	return &envoy_core_v3.DataSource{
+		Specifier: &envoy_core_v3.DataSource_InlineBytes{
+			InlineBytes: bytes,
+		},
+	}
+}
+
+func MeshSpiffeIDPrefixMatcher(mesh string) *envoy_type_matcher_v3.StringMatcher {
+	return &envoy_type_matcher_v3.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v3.StringMatcher_Prefix{
+			Prefix: tls.MeshSpiffeIDPrefix(mesh),
+		},
+	}
+}
+
+func ServiceSpiffeIDMatcher(mesh string, service string) *envoy_type_matcher_v3.StringMatcher {
+	return &envoy_type_matcher_v3.StringMatcher{
+		MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
+			Exact: tls.ServiceSpiffeID(mesh, service),
+		},
+	}
+}
diff --git a/pkg/xds/envoy/types.go b/pkg/xds/envoy/types.go
index 7cb8f6d18..69e0de011 100644
--- a/pkg/xds/envoy/types.go
+++ b/pkg/xds/envoy/types.go
@@ -5,6 +5,7 @@ import (
 	"sort"
 	"strings"
 
+	envoy_types "github.com/envoyproxy/go-control-plane/pkg/cache/types"
 	"github.com/pkg/errors"
 )
 
@@ -138,3 +139,8 @@ func (c Clusters) Get(name string) *Cluster {
 func (c Clusters) Tags(name string) []Tags {
 	return c[name].Tags()
 }
+
+type NamedResource interface {
+	envoy_types.Resource
+	GetName() string
+}
diff --git a/pkg/xds/generator/direct_access_proxy_generator.go b/pkg/xds/generator/direct_access_proxy_generator.go
index 2508eb1fa..40171dec4 100644
--- a/pkg/xds/generator/direct_access_proxy_generator.go
+++ b/pkg/xds/generator/direct_access_proxy_generator.go
@@ -63,7 +63,7 @@ func (_ DirectAccessProxyGenerator) Generate(ctx xds_context.Context, proxy *cor
 		})
 	}
 
-	directAccessCluster, err := envoy_clusters.NewClusterBuilder().
+	directAccessCluster, err := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 		Configure(envoy_clusters.PassThroughCluster("direct_access")).
 		Configure(envoy_clusters.UnknownDestinationClientSideMTLS(ctx, proxy.Metadata)).
 		Build()
@@ -71,7 +71,7 @@ func (_ DirectAccessProxyGenerator) Generate(ctx xds_context.Context, proxy *cor
 		return nil, errors.Wrapf(err, "could not generate cluster: direct_access")
 	}
 	resources.Add(&core_xds.Resource{
-		Name:     directAccessCluster.Name,
+		Name:     directAccessCluster.GetName(),
 		Origin:   OriginDirectAccess,
 		Resource: directAccessCluster,
 	})
diff --git a/pkg/xds/generator/inbound_proxy_generator.go b/pkg/xds/generator/inbound_proxy_generator.go
index 518e2eab6..0cfc7cf01 100644
--- a/pkg/xds/generator/inbound_proxy_generator.go
+++ b/pkg/xds/generator/inbound_proxy_generator.go
@@ -32,7 +32,7 @@ func (g InboundProxyGenerator) Generate(ctx xds_context.Context, proxy *model.Pr
 
 		// generate CDS resource
 		localClusterName := envoy_names.GetLocalClusterName(endpoint.WorkloadPort)
-		clusterBuilder := envoy_clusters.NewClusterBuilder().
+		clusterBuilder := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.StaticCluster(localClusterName, endpoint.WorkloadIP, endpoint.WorkloadPort))
 		switch protocol {
 		case mesh_core.ProtocolHTTP2, mesh_core.ProtocolGRPC:
diff --git a/pkg/xds/generator/ingress_generator.go b/pkg/xds/generator/ingress_generator.go
index 239a448f6..2403462e6 100644
--- a/pkg/xds/generator/ingress_generator.go
+++ b/pkg/xds/generator/ingress_generator.go
@@ -13,7 +13,7 @@ import (
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 	envoy_common "github.com/kumahq/kuma/pkg/xds/envoy"
 	envoy_clusters "github.com/kumahq/kuma/pkg/xds/envoy/clusters"
-	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	envoy_endpoints "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 	envoy_listeners "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
 	envoy_names "github.com/kumahq/kuma/pkg/xds/envoy/names"
 )
@@ -126,7 +126,7 @@ func (_ IngressGenerator) services(proxy *model.Proxy) []string {
 
 func (i IngressGenerator) generateCDS(services []string, destinationsPerService map[string][]envoy_common.Tags) (resources []*model.Resource, _ error) {
 	for _, service := range services {
-		edsCluster, err := envoy_clusters.NewClusterBuilder().
+		edsCluster, err := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.EdsCluster(service)).
 			Configure(envoy_clusters.LbSubset(i.lbSubsets(service, destinationsPerService))).
 			Build()
diff --git a/pkg/xds/generator/outbound_proxy_generator.go b/pkg/xds/generator/outbound_proxy_generator.go
index 8e046ece7..5b4e5f2ab 100644
--- a/pkg/xds/generator/outbound_proxy_generator.go
+++ b/pkg/xds/generator/outbound_proxy_generator.go
@@ -132,7 +132,7 @@ func (o OutboundProxyGenerator) generateCDS(ctx xds_context.Context, proxy *mode
 		tags := clusters.Tags(clusterName)
 		healthCheck := proxy.HealthChecks[serviceName]
 		circuitBreaker := proxy.CircuitBreakers[serviceName]
-		edsClusterBuilder := envoy_clusters.NewClusterBuilder().
+		edsClusterBuilder := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.LbSubset(o.lbSubsets(tags))).
 			Configure(envoy_clusters.OutlierDetection(circuitBreaker)).
 			Configure(envoy_clusters.HealthCheck(healthCheck))
diff --git a/pkg/xds/generator/prometheus_endpoint_generator.go b/pkg/xds/generator/prometheus_endpoint_generator.go
index 75e41273e..dabfbf3b3 100644
--- a/pkg/xds/generator/prometheus_endpoint_generator.go
+++ b/pkg/xds/generator/prometheus_endpoint_generator.go
@@ -11,6 +11,7 @@ import (
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	envoy_clusters "github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 	envoy_listeners "github.com/kumahq/kuma/pkg/xds/envoy/listeners"
 	envoy_names "github.com/kumahq/kuma/pkg/xds/envoy/names"
@@ -100,7 +101,7 @@ func (g PrometheusEndpointGenerator) Generate(ctx xds_context.Context, proxy *co
 	if err != nil {
 		return nil, err
 	}
-	cluster, err := envoy_clusters.NewClusterBuilder().
+	cluster, err := envoy_clusters.NewClusterBuilder(envoy.APIV2).
 		Configure(envoy_clusters.StaticCluster(envoyAdminClusterName, adminAddress, adminPort)).
 		Build()
 	if err != nil {
@@ -108,7 +109,7 @@ func (g PrometheusEndpointGenerator) Generate(ctx xds_context.Context, proxy *co
 	}
 	resources := core_xds.NewResourceSet()
 	resources.Add(&core_xds.Resource{
-		Name:     cluster.Name,
+		Name:     cluster.GetName(),
 		Origin:   OriginPrometheus,
 		Resource: cluster,
 	})
diff --git a/pkg/xds/generator/tracing_proxy_generator.go b/pkg/xds/generator/tracing_proxy_generator.go
index 960cc74e7..87be65043 100644
--- a/pkg/xds/generator/tracing_proxy_generator.go
+++ b/pkg/xds/generator/tracing_proxy_generator.go
@@ -10,6 +10,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/util/proto"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 	"github.com/kumahq/kuma/pkg/xds/envoy/names"
 )
@@ -53,7 +54,7 @@ func (t TracingProxyGenerator) zipkinCluster(backend *mesh_proto.TracingBackend)
 	}
 
 	clusterName := names.GetTracingClusterName(backend.Name)
-	cluster, err := clusters.NewClusterBuilder().
+	cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 		Configure(clusters.DNSCluster(clusterName, url.Hostname(), uint32(port))).
 		Build()
 	if err != nil {
diff --git a/pkg/xds/generator/transparent_proxy_generator.go b/pkg/xds/generator/transparent_proxy_generator.go
index 1f8c18da4..8f0cd2194 100644
--- a/pkg/xds/generator/transparent_proxy_generator.go
+++ b/pkg/xds/generator/transparent_proxy_generator.go
@@ -31,12 +31,12 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 	sourceService := proxy.Dataplane.Spec.GetIdentifyingService()
 	meshName := ctx.Mesh.Resource.GetMeta().GetName()
 
-	var outboundPassThroughCluster *envoy_api_v2.Cluster = nil
+	var outboundPassThroughCluster envoy_common.NamedResource = nil
 	var outboundListener *envoy_api_v2.Listener = nil
 	var err error
 
 	if ctx.Mesh.Resource.Spec.IsPassthrough() {
-		outboundPassThroughCluster, err = envoy_clusters.NewClusterBuilder().
+		outboundPassThroughCluster, err = envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 			Configure(envoy_clusters.PassThroughCluster(outboundName)).
 			Build()
 		if err != nil {
@@ -57,7 +57,7 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 
 	redirectPortInbound := proxy.Dataplane.Spec.Networking.GetTransparentProxying().GetRedirectPortInbound()
 
-	inboundPassThroughCluster, err := envoy_clusters.NewClusterBuilder().
+	inboundPassThroughCluster, err := envoy_clusters.NewClusterBuilder(envoy_common.APIV2).
 		Configure(envoy_clusters.PassThroughCluster(inboundName)).
 		Configure(envoy_clusters.UpstreamBindConfig("127.0.0.6", 0)).
 		Build()
@@ -83,7 +83,7 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 
 	if ctx.Mesh.Resource.Spec.IsPassthrough() {
 		resources.Add(&model.Resource{
-			Name:     outboundPassThroughCluster.Name,
+			Name:     outboundPassThroughCluster.GetName(),
 			Origin:   OriginTransparent,
 			Resource: outboundPassThroughCluster,
 		})
@@ -94,7 +94,7 @@ func (_ TransparentProxyGenerator) Generate(ctx xds_context.Context, proxy *mode
 		Resource: inboundListener,
 	})
 	resources.Add(&model.Resource{
-		Name:     inboundPassThroughCluster.Name,
+		Name:     inboundPassThroughCluster.GetName(),
 		Origin:   OriginTransparent,
 		Resource: inboundPassThroughCluster,
 	})
diff --git a/pkg/xds/topology/outbound.go b/pkg/xds/topology/outbound.go
index 4a6950bd2..4865776b2 100644
--- a/pkg/xds/topology/outbound.go
+++ b/pkg/xds/topology/outbound.go
@@ -4,8 +4,6 @@ import (
 	"context"
 	"fmt"
 
-	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
-
 	"github.com/kumahq/kuma/api/system/v1alpha1"
 
 	"github.com/kumahq/kuma/pkg/core"
@@ -171,7 +169,7 @@ func buildExternalServiceEndpoint(externalService *mesh_core.ExternalServiceReso
 	}, nil
 }
 
-func convertToEnvoy(ds *v1alpha1.DataSource, mesh string, loader datasource.Loader) *envoy_core.DataSource {
+func convertToEnvoy(ds *v1alpha1.DataSource, mesh string, loader datasource.Loader) []byte {
 	if ds == nil {
 		return nil
 	}
@@ -182,11 +180,7 @@ func convertToEnvoy(ds *v1alpha1.DataSource, mesh string, loader datasource.Load
 		return nil
 	}
 
-	return &envoy_core.DataSource{
-		Specifier: &envoy_core.DataSource_InlineBytes{
-			InlineBytes: data,
-		},
-	}
+	return data
 }
 
 func localityFromTags(mesh *mesh_core.MeshResource, priority uint32, tags map[string]string) *core_xds.Locality {


Test patch:
diff --git a/pkg/plugins/runtime/k8s/probes/probe_test.go b/pkg/plugins/runtime/k8s/probes/probe_test.go
index 76f52c863..aac214f92 100644
--- a/pkg/plugins/runtime/k8s/probes/probe_test.go
+++ b/pkg/plugins/runtime/k8s/probes/probe_test.go
@@ -62,4 +62,22 @@ var _ = Describe("KumaProbe", func() {
 				"to 9000. It is reserved for the dataplane that will serve pods without mTLS."))
 		})
 	})
+
+	Context("Prepend /", func() {
+		It("should convert to path with prepended /", func() {
+			podProbeYaml := `
+                httpGet:
+                  path: c1/hc
+                  port: 8080
+`
+			probe := kube_core.Probe{}
+			err := yaml.Unmarshal([]byte(podProbeYaml), &probe)
+			Expect(err).ToNot(HaveOccurred())
+
+			virtual, err := probes.KumaProbe(probe).ToVirtual(9000)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(virtual.Path()).To(Equal("/8080/c1/hc"))
+			Expect(virtual.Port()).To(Equal(uint32(9000)))
+		})
+	})
 })
diff --git a/pkg/util/proto/any_test.go b/pkg/util/proto/any_test.go
index 2a2f2bbde..0c1ec94d1 100644
--- a/pkg/util/proto/any_test.go
+++ b/pkg/util/proto/any_test.go
@@ -4,7 +4,7 @@ import (
 	"github.com/golang/protobuf/proto"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
-	"github.com/kumahq/kuma/pkg/xds/envoy"
+	envoy_metadata "github.com/kumahq/kuma/pkg/xds/envoy/metadata/v2"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -17,7 +17,7 @@ var _ = Describe("MarshalAnyDeterministic", func() {
 			"version": "v1",
 			"cloud":   "aws",
 		}
-		metadata := envoy.EndpointMetadata(tags)
+		metadata := envoy_metadata.EndpointMetadata(tags)
 		for i := 0; i < 100; i++ {
 			any1, _ := util_proto.MarshalAnyDeterministic(metadata)
 			any2, _ := util_proto.MarshalAnyDeterministic(metadata)
diff --git a/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go
new file mode 100644
index 000000000..4cffc602f
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v2/circuit_breaker_configurer_test.go
@@ -0,0 +1,248 @@
+package clusters_test
+
+import (
+	"github.com/golang/protobuf/ptypes/wrappers"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("OutlierDetectionConfigurer", func() {
+
+	type testCase struct {
+		clusterName    string
+		circuitBreaker *mesh_core.CircuitBreakerResource
+		expected       string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.OutlierDetection(given.circuitBreaker)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("CircuitBreaker with TotalError detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							TotalErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 100
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with GatewayError detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							GatewayErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 100
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with LocalError detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							LocalErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 100
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with all error detectors, custom values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							TotalErrors:   &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{Consecutive: &wrappers.UInt32Value{Value: 21}},
+							GatewayErrors: &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{Consecutive: &wrappers.UInt32Value{Value: 11}},
+							LocalErrors:   &mesh_proto.CircuitBreaker_Conf_Detectors_Errors{Consecutive: &wrappers.UInt32Value{Value: 6}},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              consecutive5xx: 21
+              consecutiveGatewayFailure: 11
+              consecutiveLocalOriginFailure: 6
+              enforcingConsecutive5xx: 100
+              enforcingConsecutiveGatewayFailure: 100
+              enforcingConsecutiveLocalOriginFailure: 100
+              enforcingFailurePercentage: 0
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with StandardDeviation detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							StandardDeviation: &mesh_proto.CircuitBreaker_Conf_Detectors_StandardDeviation{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingLocalOriginSuccessRate: 100
+              enforcingSuccessRate: 100
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with StandardDeviation detector, custom values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							StandardDeviation: &mesh_proto.CircuitBreaker_Conf_Detectors_StandardDeviation{
+								RequestVolume: &wrappers.UInt32Value{Value: 7},
+								MinimumHosts:  &wrappers.UInt32Value{Value: 8},
+								Factor:        &wrappers.DoubleValue{Value: 1.9},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentage: 0
+              enforcingLocalOriginSuccessRate: 100
+              enforcingSuccessRate: 100
+              successRateMinimumHosts: 8
+              successRateRequestVolume: 7
+              successRateStdevFactor: 1900
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with Failure detector, default values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							Failure: &mesh_proto.CircuitBreaker_Conf_Detectors_Failure{},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentageLocalOrigin: 100
+              enforcingFailurePercentage: 100
+              enforcingSuccessRate: 0
+            type: EDS`,
+		}),
+		Entry("CircuitBreaker with Failure detector, custom values", testCase{
+			circuitBreaker: &mesh_core.CircuitBreakerResource{
+				Spec: &mesh_proto.CircuitBreaker{
+					Conf: &mesh_proto.CircuitBreaker_Conf{
+						Detectors: &mesh_proto.CircuitBreaker_Conf_Detectors{
+							Failure: &mesh_proto.CircuitBreaker_Conf_Detectors_Failure{
+								RequestVolume: &wrappers.UInt32Value{Value: 7},
+								MinimumHosts:  &wrappers.UInt32Value{Value: 8},
+								Threshold:     &wrappers.UInt32Value{Value: 85},
+							},
+						},
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            outlierDetection:
+              enforcingConsecutive5xx: 0
+              enforcingConsecutiveGatewayFailure: 0
+              enforcingConsecutiveLocalOriginFailure: 0
+              enforcingFailurePercentageLocalOrigin: 100
+              enforcingFailurePercentage: 100
+              enforcingSuccessRate: 0
+              failurePercentageMinimumHosts: 8
+              failurePercentageRequestVolume: 7
+              failurePercentageThreshold: 85
+            type: EDS`,
+		}),
+	)
+
+})
diff --git a/pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
similarity index 99%
rename from pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
index 432b75b6c..ae3e381f7 100644
--- a/pkg/xds/envoy/clusters/client_side_mtls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_mtls_configurer_test.go
@@ -29,7 +29,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.ClientSideMTLS(given.ctx, given.metadata, given.clientService, given.tags)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/client_side_tls_configurer_test.go b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
similarity index 93%
rename from pkg/xds/envoy/clusters/client_side_tls_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
index 91b76aaec..3c6315a61 100644
--- a/pkg/xds/envoy/clusters/client_side_tls_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/client_side_tls_configurer_test.go
@@ -6,6 +6,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
@@ -22,7 +23,7 @@ var _ = Describe("ClientSideTLSConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.ClientSideTLS(given.endpoints)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/clusters_suite_test.go b/pkg/xds/envoy/clusters/v2/clusters_suite_test.go
similarity index 83%
rename from pkg/xds/envoy/clusters/clusters_suite_test.go
rename to pkg/xds/envoy/clusters/v2/clusters_suite_test.go
index c1d91dd19..86ed027bc 100644
--- a/pkg/xds/envoy/clusters/clusters_suite_test.go
+++ b/pkg/xds/envoy/clusters/v2/clusters_suite_test.go
@@ -9,5 +9,5 @@ import (
 
 func TestClusters(t *testing.T) {
 	RegisterFailHandler(Fail)
-	RunSpecs(t, "Clusters Suite")
+	RunSpecs(t, "Clusters V2 Suite")
 }
diff --git a/pkg/xds/envoy/clusters/dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
similarity index 91%
rename from pkg/xds/envoy/clusters/dns_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
index 14ebcedc0..5604cbd03 100644
--- a/pkg/xds/envoy/clusters/dns_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/dns_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -31,7 +32,7 @@ var _ = Describe("DNSClusterConfigurer", func() {
         type: STRICT_DNS`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.DNSCluster(clusterName, address, port)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/eds_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
similarity index 88%
rename from pkg/xds/envoy/clusters/eds_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
index 828251da2..0f8544c10 100644
--- a/pkg/xds/envoy/clusters/eds_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/eds_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -23,7 +24,7 @@ var _ = Describe("EdsClusterConfigurer", func() {
         type: EDS`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.EdsCluster(clusterName)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/health_check_configurer_test.go b/pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
similarity index 95%
rename from pkg/xds/envoy/clusters/health_check_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
index 164cb14e5..7b563afa1 100644
--- a/pkg/xds/envoy/clusters/health_check_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/health_check_configurer_test.go
@@ -8,6 +8,7 @@ import (
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 
 	. "github.com/onsi/ginkgo"
@@ -26,7 +27,7 @@ var _ = Describe("HealthCheckConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.HealthCheck(given.healthCheck)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/http2_configurer_test.go b/pkg/xds/envoy/clusters/v2/http2_configurer_test.go
similarity index 85%
rename from pkg/xds/envoy/clusters/http2_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/http2_configurer_test.go
index e9902aaa7..7d9042bae 100644
--- a/pkg/xds/envoy/clusters/http2_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/http2_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -15,7 +16,7 @@ var _ = Describe("Http2Configurer", func() {
 		expected := `http2ProtocolOptions: {}`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.Http2()).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/lb_subset_configurer_test.go b/pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
similarity index 94%
rename from pkg/xds/envoy/clusters/lb_subset_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
index 7a59e5e77..b47305147 100644
--- a/pkg/xds/envoy/clusters/lb_subset_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/lb_subset_configurer_test.go
@@ -2,6 +2,7 @@ package clusters_test
 
 import (
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 
 	. "github.com/onsi/ginkgo"
@@ -20,7 +21,7 @@ var _ = Describe("LbSubset", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.LbSubset(given.tags)).
 				Build()
diff --git a/pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
similarity index 88%
rename from pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
index 78c7abb23..b019440c1 100644
--- a/pkg/xds/envoy/clusters/pass_through_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/pass_through_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -21,7 +22,7 @@ var _ = Describe("PassThroughClusterConfigurer", func() {
         type: ORIGINAL_DST`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.PassThroughCluster(clusterName)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/static_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
similarity index 91%
rename from pkg/xds/envoy/clusters/static_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
index 69772512d..a706099fb 100644
--- a/pkg/xds/envoy/clusters/static_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/static_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
@@ -31,7 +32,7 @@ var _ = Describe("StaticClusterConfigurer", func() {
         type: STATIC`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.StaticCluster(clusterName, address, port)).
 			Build()
 
diff --git a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
similarity index 92%
rename from pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go
rename to pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
index a4f9d9fbb..e1d80e95f 100644
--- a/pkg/xds/envoy/clusters/strict_dns_cluster_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v2/strict_dns_cluster_configurer_test.go
@@ -5,6 +5,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
@@ -34,7 +35,7 @@ var _ = Describe("StrictDNSClusterConfigurer", func() {
         type: STRICT_DNS`
 
 		// when
-		cluster, err := clusters.NewClusterBuilder().
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV2).
 			Configure(clusters.StrictDNSCluster(clusterName, []xds.Endpoint{
 				{
 					Target: address,
diff --git a/pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
similarity index 97%
rename from pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go
rename to pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
index 7448621ab..de9323588 100644
--- a/pkg/xds/envoy/clusters/circuit_breaker_configurer_test.go
+++ b/pkg/xds/envoy/clusters/v3/circuit_breaker_configurer_test.go
@@ -2,17 +2,18 @@ package clusters_test
 
 import (
 	"github.com/golang/protobuf/ptypes/wrappers"
-	"github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
 	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
 )
 
-var _ = ginkgo.Describe("OutlierDetectionConfigurer", func() {
+var _ = Describe("OutlierDetectionConfigurer", func() {
 
 	type testCase struct {
 		clusterName    string
@@ -23,7 +24,7 @@ var _ = ginkgo.Describe("OutlierDetectionConfigurer", func() {
 	DescribeTable("should generate proper Envoy config",
 		func(given testCase) {
 			// when
-			cluster, err := clusters.NewClusterBuilder().
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
 				Configure(clusters.EdsCluster(given.clusterName)).
 				Configure(clusters.OutlierDetection(given.circuitBreaker)).
 				Build()
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
new file mode 100644
index 000000000..e7dc1c563
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_mtls_configurer_test.go
@@ -0,0 +1,342 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("EdsClusterConfigurer", func() {
+
+	type testCase struct {
+		clusterName   string
+		clientService string
+		tags          []envoy.Tags
+		ctx           xds_context.Context
+		metadata      *core_xds.DataplaneMetadata
+		expected      string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.ClientSideMTLS(given.ctx, given.metadata, given.clientService, given.tags)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("cluster with mTLS", testCase{
+			clusterName:   "testCluster",
+			clientService: "backend",
+			ctx: xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-control-plane:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert: []byte("CERTIFICATE"),
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			// no tags therefore SNI is empty
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            transportSocket:
+              name: envoy.transport_sockets.tls
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+            type: EDS`,
+		}),
+		Entry("cluster with many different tag sets", testCase{
+			clusterName:   "testCluster",
+			clientService: "backend",
+			ctx: xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-control-plane:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert: []byte("CERTIFICATE"),
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			tags: []envoy.Tags{
+				map[string]string{
+					"kuma.io/service": "backend",
+					"cluster":         "1",
+				},
+				map[string]string{
+					"kuma.io/service": "backend",
+					"cluster":         "2",
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            transportSocketMatches:
+            - match:
+                cluster: "1"
+              name: backend{cluster=1,mesh=default}
+              transportSocket:
+                name: envoy.transport_sockets.tls
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                  commonTlsContext:
+                    combinedValidationContext:
+                      defaultValidationContext:
+                        matchSubjectAltNames:
+                        - exact: spiffe://default/backend
+                      validationContextSdsSecretConfig:
+                        name: mesh_ca
+                        sdsConfig:
+                          apiConfigSource:
+                            apiType: GRPC
+                            grpcServices:
+                            - googleGrpc:
+                                channelCredentials:
+                                  sslCredentials:
+                                    rootCerts:
+                                      inlineBytes: Q0VSVElGSUNBVEU=
+                                statPrefix: sds_mesh_ca
+                                targetUri: kuma-control-plane:5677
+                    tlsCertificateSdsSecretConfigs:
+                    - name: identity_cert
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_identity_cert
+                              targetUri: kuma-control-plane:5677
+                  sni: backend{cluster=1,mesh=default}
+            - match:
+                cluster: "2"
+              name: backend{cluster=2,mesh=default}
+              transportSocket:
+                name: envoy.transport_sockets.tls
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                  commonTlsContext:
+                    combinedValidationContext:
+                      defaultValidationContext:
+                        matchSubjectAltNames:
+                        - exact: spiffe://default/backend
+                      validationContextSdsSecretConfig:
+                        name: mesh_ca
+                        sdsConfig:
+                          apiConfigSource:
+                            apiType: GRPC
+                            grpcServices:
+                            - googleGrpc:
+                                channelCredentials:
+                                  sslCredentials:
+                                    rootCerts:
+                                      inlineBytes: Q0VSVElGSUNBVEU=
+                                statPrefix: sds_mesh_ca
+                                targetUri: kuma-control-plane:5677
+                    tlsCertificateSdsSecretConfigs:
+                    - name: identity_cert
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_identity_cert
+                              targetUri: kuma-control-plane:5677
+                  sni: backend{cluster=2,mesh=default}
+            type: EDS`,
+		}),
+		Entry("cluster with mTLS and credentials", testCase{
+			clusterName:   "testCluster",
+			clientService: "backend",
+			ctx: xds_context.Context{
+				ConnectionInfo: xds_context.ConnectionInfo{
+					Authority: "kuma-control-plane:5677",
+				},
+				ControlPlane: &xds_context.ControlPlaneContext{
+					SdsTlsCert: []byte("CERTIFICATE"),
+				},
+				Mesh: xds_context.MeshContext{
+					Resource: &mesh_core.MeshResource{
+						Meta: &test_model.ResourceMeta{
+							Name: "default",
+						},
+						Spec: &mesh_proto.Mesh{
+							Mtls: &mesh_proto.Mesh_Mtls{
+								EnabledBackend: "builtin",
+								Backends: []*mesh_proto.CertificateAuthorityBackend{
+									{
+										Name: "builtin",
+										Type: "builtin",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			metadata: &core_xds.DataplaneMetadata{
+				DataplaneTokenPath: "/var/secret/token",
+			},
+			tags: []envoy.Tags{
+				{
+					"kuma.io/service": "backend",
+					"version":         "v1",
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            transportSocket:
+              name: envoy.transport_sockets.tls
+              typedConfig:
+                '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              callCredentials:
+                              - fromPlugin:
+                                  name: envoy.grpc_credentials.file_based_metadata
+                                  typedConfig:
+                                    '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                    secretData:
+                                      filename: /var/secret/token
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            callCredentials:
+                            - fromPlugin:
+                                name: envoy.grpc_credentials.file_based_metadata
+                                typedConfig:
+                                  '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                  secretData:
+                                    filename: /var/secret/token
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                sni: backend{mesh=default,version=v1}
+            type: EDS`,
+		}),
+	)
+})
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
new file mode 100644
index 000000000..0380aa1f6
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/client_side_tls_configurer_test.go
@@ -0,0 +1,69 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("ClientSideTLSConfigurer", func() {
+
+	type testCase struct {
+		clusterName string
+		endpoints   []xds.Endpoint
+		expected    string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.ClientSideTLS(given.endpoints)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("cluster with mTLS", testCase{
+			clusterName: "testCluster",
+			endpoints: []xds.Endpoint{
+				{
+					Target: "httpbin.org",
+					Port:   3000,
+					Tags:   nil,
+					Weight: 100,
+					ExternalService: &xds.ExternalService{
+						TLSEnabled: true,
+					},
+				},
+			},
+
+			expected: `
+        connectTimeout: 5s
+        edsClusterConfig:
+          edsConfig:
+            ads: {}
+        name: testCluster
+        transportSocketMatches:
+        - match: {}
+          name: httpbin.org
+          transportSocket:
+            name: envoy.transport_sockets.tls
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+              commonTlsContext: {}
+        type: EDS
+`}),
+	)
+})
diff --git a/pkg/xds/envoy/clusters/v3/clusters_suite_test.go b/pkg/xds/envoy/clusters/v3/clusters_suite_test.go
new file mode 100644
index 000000000..0b1e17e78
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/clusters_suite_test.go
@@ -0,0 +1,13 @@
+package clusters_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestClusters(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Clusters V3 Suite")
+}
diff --git a/pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go
new file mode 100644
index 000000000..56af100cd
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/dns_cluster_configurer_test.go
@@ -0,0 +1,46 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("DNSClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		address := "google.com"
+		port := uint32(80)
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        loadAssignment:
+          clusterName: test:cluster
+          endpoints:
+          - lbEndpoints:
+            - endpoint:
+                address:
+                  socketAddress:
+                    address: google.com
+                    portValue: 80
+        name: test:cluster
+        type: STRICT_DNS`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.DNSCluster(clusterName, address, port)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
new file mode 100644
index 000000000..20a2e7c6f
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/eds_cluster_configurer_test.go
@@ -0,0 +1,38 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("EdsClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        edsClusterConfig:
+          edsConfig:
+            ads: {}
+        name: test:cluster
+        type: EDS`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.EdsCluster(clusterName)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
new file mode 100644
index 000000000..449a6b578
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/health_check_configurer_test.go
@@ -0,0 +1,86 @@
+package clusters_test
+
+import (
+	"time"
+
+	"github.com/golang/protobuf/ptypes"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("HealthCheckConfigurer", func() {
+
+	type testCase struct {
+		clusterName string
+		healthCheck *mesh_core.HealthCheckResource
+		expected    string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.HealthCheck(given.healthCheck)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("HealthCheck with neither active nor passive checks", testCase{
+			clusterName: "testCluster",
+			healthCheck: mesh_core.NewHealthCheckResource(),
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: testCluster
+            type: EDS`,
+		}),
+		Entry("HealthCheck with active checks", testCase{
+			clusterName: "testCluster",
+			healthCheck: &mesh_core.HealthCheckResource{
+				Spec: &mesh_proto.HealthCheck{
+					Sources: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "backend"}},
+					},
+					Destinations: []*mesh_proto.Selector{
+						{Match: mesh_proto.TagSelector{"kuma.io/service": "redis"}},
+					},
+					Conf: &mesh_proto.HealthCheck_Conf{
+						Interval:           ptypes.DurationProto(5 * time.Second),
+						Timeout:            ptypes.DurationProto(4 * time.Second),
+						UnhealthyThreshold: 3,
+						HealthyThreshold:   2,
+					},
+				},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            healthChecks:
+            - healthyThreshold: 2
+              interval: 5s
+              tcpHealthCheck: {}
+              timeout: 4s
+              unhealthyThreshold: 3
+            name: testCluster
+            type: EDS`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/clusters/v3/http2_configurer_test.go b/pkg/xds/envoy/clusters/v3/http2_configurer_test.go
new file mode 100644
index 000000000..39532bc1d
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/http2_configurer_test.go
@@ -0,0 +1,35 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("Http2Configurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		expected := `
+        typedExtensionProtocolOptions:
+          envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
+            '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
+            explicitHttpConfig:
+              http2ProtocolOptions: {}`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.Http2()).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
new file mode 100644
index 000000000..08aaa8b69
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/lb_subset_configurer_test.go
@@ -0,0 +1,72 @@
+package clusters_test
+
+import (
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("LbSubset", func() {
+
+	type testCase struct {
+		clusterName string
+		tags        [][]string
+		expected    string
+	}
+
+	DescribeTable("should generate proper Envoy config",
+		func(given testCase) {
+			// when
+			cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+				Configure(clusters.EdsCluster(given.clusterName)).
+				Configure(clusters.LbSubset(given.tags)).
+				Build()
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			actual, err := util_proto.ToYAML(cluster)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("LbSubset is empty if there are no tags", testCase{
+			clusterName: "backend",
+			tags:        [][]string{},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            name: backend
+            type: EDS`,
+		}),
+		Entry("LbSubset is set when more than service tag is set", testCase{
+			clusterName: "backend",
+			tags: [][]string{
+				{"version"},
+				{"cluster", "version"},
+			},
+			expected: `
+            connectTimeout: 5s
+            edsClusterConfig:
+              edsConfig:
+                ads: {}
+            lbSubsetConfig:
+              fallbackPolicy: ANY_ENDPOINT
+              subsetSelectors:
+              - fallbackPolicy: NO_FALLBACK
+                keys:
+                - version
+              - fallbackPolicy: NO_FALLBACK
+                keys:
+                - cluster
+                - version
+            name: backend
+            type: EDS`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go
new file mode 100644
index 000000000..334e3a603
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/pass_through_cluster_configurer_test.go
@@ -0,0 +1,36 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("PassThroughClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        lbPolicy: CLUSTER_PROVIDED
+        name: test:cluster
+        type: ORIGINAL_DST`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.PassThroughCluster(clusterName)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go
new file mode 100644
index 000000000..fc88b0d1c
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/static_cluster_configurer_test.go
@@ -0,0 +1,46 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("StaticClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		address := "192.168.0.1"
+		port := uint32(8080)
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        loadAssignment:
+          clusterName: test:cluster
+          endpoints:
+          - lbEndpoints:
+            - endpoint:
+                address:
+                  socketAddress:
+                    address: 192.168.0.1
+                    portValue: 8080
+        name: test:cluster
+        type: STATIC`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.StaticCluster(clusterName, address, port)).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go
new file mode 100644
index 000000000..fd2509c08
--- /dev/null
+++ b/pkg/xds/envoy/clusters/v3/strict_dns_cluster_configurer_test.go
@@ -0,0 +1,59 @@
+package clusters_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/kumahq/kuma/pkg/core/xds"
+	"github.com/kumahq/kuma/pkg/xds/envoy"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+	"github.com/kumahq/kuma/pkg/xds/envoy/clusters"
+)
+
+var _ = Describe("StrictDNSClusterConfigurer", func() {
+
+	It("should generate proper Envoy config", func() {
+		// given
+		clusterName := "test:cluster"
+		address := "google.com"
+		port := uint32(80)
+		expected := `
+        altStatName: test_cluster
+        connectTimeout: 5s
+        loadAssignment:
+          clusterName: test:cluster
+          endpoints:
+          - lbEndpoints:
+            - endpoint:
+                address:
+                  socketAddress:
+                    address: google.com
+                    portValue: 80
+              loadBalancingWeight: 100
+        name: test:cluster
+        type: STRICT_DNS`
+
+		// when
+		cluster, err := clusters.NewClusterBuilder(envoy.APIV3).
+			Configure(clusters.StrictDNSCluster(clusterName, []xds.Endpoint{
+				{
+					Target: address,
+					Port:   port,
+					Tags:   nil,
+					Weight: 100,
+					ExternalService: &xds.ExternalService{
+						TLSEnabled: true,
+					},
+				},
+			})).
+			Build()
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		actual, err := util_proto.ToYAML(cluster)
+		Expect(err).ToNot(HaveOccurred())
+		Expect(actual).To(MatchYAML(expected))
+	})
+})
diff --git a/pkg/xds/envoy/endpoints/endpoints_suite_test.go b/pkg/xds/envoy/endpoints/v2/endpoints_suite_test.go
similarity index 100%
rename from pkg/xds/envoy/endpoints/endpoints_suite_test.go
rename to pkg/xds/envoy/endpoints/v2/endpoints_suite_test.go
diff --git a/pkg/xds/envoy/endpoints/endpoints_test.go b/pkg/xds/envoy/endpoints/v2/endpoints_test.go
similarity index 99%
rename from pkg/xds/envoy/endpoints/endpoints_test.go
rename to pkg/xds/envoy/endpoints/v2/endpoints_test.go
index 1af754d6d..c7b344f64 100644
--- a/pkg/xds/envoy/endpoints/endpoints_test.go
+++ b/pkg/xds/envoy/endpoints/v2/endpoints_test.go
@@ -5,7 +5,7 @@ import (
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
-	. "github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	. "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 
diff --git a/pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go b/pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go
new file mode 100644
index 000000000..7eb53a0de
--- /dev/null
+++ b/pkg/xds/envoy/endpoints/v3/endpoints_suite_test.go
@@ -0,0 +1,13 @@
+package endpoints_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestEndpoints(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Endpoints Suite")
+}
diff --git a/pkg/xds/envoy/endpoints/v3/endpoints_test.go b/pkg/xds/envoy/endpoints/v3/endpoints_test.go
new file mode 100644
index 000000000..d67fc1154
--- /dev/null
+++ b/pkg/xds/envoy/endpoints/v3/endpoints_test.go
@@ -0,0 +1,220 @@
+package endpoints_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	. "github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v3"
+
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+var _ = Describe("Endpoints", func() {
+
+	Describe("CreateStaticEndpoint()", func() {
+		It("should generate 'static' Endpoints", func() {
+			// given
+			expected := `
+            clusterName: localhost:8080
+            endpoints:
+            - lbEndpoints:
+              - endpoint:
+                  address:
+                    socketAddress:
+                      address: 127.0.0.1
+                      portValue: 8080
+`
+			// when
+			resource := CreateStaticEndpoint("localhost:8080", "127.0.0.1", 8080)
+
+			// then
+			actual, err := util_proto.ToYAML(resource)
+
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(expected))
+		})
+	})
+
+	Describe("ClusterLoadAssignment()", func() {
+		type testCase struct {
+			cluster   string
+			endpoints []core_xds.Endpoint
+			expected  string
+		}
+		DescribeTable("should generate ClusterLoadAssignment",
+			func(given testCase) {
+				// when
+				resource := CreateClusterLoadAssignment(given.cluster, given.endpoints)
+
+				// then
+				actual, err := util_proto.ToYAML(resource)
+
+				Expect(err).ToNot(HaveOccurred())
+				Expect(actual).To(MatchYAML(given.expected))
+			},
+			Entry("without tags", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Weight: 2,
+					},
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Weight: 1,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    loadBalancingWeight: 2
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    loadBalancingWeight: 1
+`,
+			}),
+			Entry("stable output", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{ // endpoints in different order, but result is the same as in previous test
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Weight: 1,
+					},
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Weight: 2,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    loadBalancingWeight: 2
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    loadBalancingWeight: 1
+`,
+			}),
+			Entry("with tags", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Tags:   map[string]string{"kuma.io/service": "backend", "region": "us"},
+						Weight: 1,
+					},
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Tags:   map[string]string{"kuma.io/service": "backend", "region": "eu"},
+						Weight: 2,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          region: us
+                        envoy.transport_socket_match:
+                          region: us
+                    loadBalancingWeight: 1
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          region: eu
+                        envoy.transport_socket_match:
+                          region: eu
+                    loadBalancingWeight: 2
+`,
+			}),
+			Entry("with locality tags", testCase{
+				cluster: "127.0.0.1:8080",
+				endpoints: []core_xds.Endpoint{
+					{
+						Target: "192.168.0.1",
+						Port:   8081,
+						Tags:   map[string]string{"kuma.io/service": "backend", "kuma.io/region": "us", "kuma.io/zone": "west"},
+						Weight: 1,
+					},
+					{
+						Target: "192.168.0.2",
+						Port:   8082,
+						Tags:   map[string]string{"kuma.io/service": "backend", "kuma.io/region": "eu", "kuma.io/zone": "west"},
+						Weight: 2,
+					},
+				},
+				expected: `
+                clusterName: 127.0.0.1:8080
+                endpoints:
+                - lbEndpoints:
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.1
+                          portValue: 8081
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          kuma.io/region: us
+                          kuma.io/zone: west
+                        envoy.transport_socket_match:
+                          kuma.io/region: us
+                          kuma.io/zone: west
+                    loadBalancingWeight: 1
+                  - endpoint:
+                      address:
+                        socketAddress:
+                          address: 192.168.0.2
+                          portValue: 8082
+                    metadata:
+                      filterMetadata:
+                        envoy.lb:
+                          kuma.io/region: eu
+                          kuma.io/zone: west
+                        envoy.transport_socket_match:
+                          kuma.io/region: eu
+                          kuma.io/zone: west
+                    loadBalancingWeight: 2
+`,
+			}),
+		)
+	})
+})
diff --git a/pkg/xds/envoy/metadata/v2/metadata_suite_test.go b/pkg/xds/envoy/metadata/v2/metadata_suite_test.go
new file mode 100644
index 000000000..c63d1da5b
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v2/metadata_suite_test.go
@@ -0,0 +1,13 @@
+package envoy_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestMetadata(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Envoy Metadata V2 Suite")
+}
diff --git a/pkg/xds/envoy/metadata/v3/metadata_suite_test.go b/pkg/xds/envoy/metadata/v3/metadata_suite_test.go
new file mode 100644
index 000000000..51f307449
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v3/metadata_suite_test.go
@@ -0,0 +1,13 @@
+package envoy_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestMetadata(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Envoy Metadata V3 Suite")
+}
diff --git a/pkg/xds/envoy/metadata/v3/metadata_test.go b/pkg/xds/envoy/metadata/v3/metadata_test.go
new file mode 100644
index 000000000..2c27fce8e
--- /dev/null
+++ b/pkg/xds/envoy/metadata/v3/metadata_test.go
@@ -0,0 +1,66 @@
+package envoy
+
+import (
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Metadata()", func() {
+
+	It("should handle `nil` map of tags", func() {
+		// when
+		metadata := EndpointMetadata(nil)
+		// then
+		Expect(metadata).To(BeNil())
+	})
+
+	It("should handle empty map of tags", func() {
+		// when
+		metadata := EndpointMetadata(map[string]string{})
+		// then
+		Expect(metadata).To(BeNil())
+	})
+
+	It("should skip service tag", func() {
+		// when
+		metadata := EndpointMetadata(map[string]string{
+			"kuma.io/service": "backend",
+		})
+		// then
+		Expect(metadata).To(BeNil())
+	})
+
+	type testCase struct {
+		tags     map[string]string
+		expected string
+	}
+	DescribeTable("should generate Envoy metadata",
+		func(given testCase) {
+			// when
+			metadata := EndpointMetadata(given.tags)
+			// and
+			actual, err := util_proto.ToYAML(metadata)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			Expect(actual).To(MatchYAML(given.expected))
+		},
+		Entry("map with multiple tags", testCase{
+			tags: map[string]string{
+				"kuma.io/service": "redis",
+				"version":         "v1",
+				"region":          "eu",
+			},
+			expected: `
+            filterMetadata:
+              envoy.lb:
+                region: eu
+                version: v1
+              envoy.transport_socket_match:
+                region: eu
+                version: v1`,
+		}),
+	)
+})
diff --git a/pkg/xds/envoy/tls/tls_suite_test.go b/pkg/xds/envoy/tls/v2/tls_suite_test.go
similarity index 94%
rename from pkg/xds/envoy/tls/tls_suite_test.go
rename to pkg/xds/envoy/tls/v2/tls_suite_test.go
index e175e8392..8f93808f3 100644
--- a/pkg/xds/envoy/tls/tls_suite_test.go
+++ b/pkg/xds/envoy/tls/v2/tls_suite_test.go
@@ -1,4 +1,4 @@
-package tls_test
+package v2_test
 
 import (
 	"testing"
diff --git a/pkg/xds/envoy/tls/tls_test.go b/pkg/xds/envoy/tls/v2/tls_test.go
similarity index 97%
rename from pkg/xds/envoy/tls/tls_test.go
rename to pkg/xds/envoy/tls/v2/tls_test.go
index 9db3f38bd..101039144 100644
--- a/pkg/xds/envoy/tls/tls_test.go
+++ b/pkg/xds/envoy/tls/v2/tls_test.go
@@ -1,17 +1,16 @@
-package tls_test
+package v2_test
 
 import (
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/tls"
-
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	v2 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v2"
 
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 )
@@ -30,7 +29,7 @@ var _ = Describe("CreateDownstreamTlsContext()", func() {
 			metadata := &core_xds.DataplaneMetadata{}
 
 			// when
-			snippet, err := tls.CreateDownstreamTlsContext(ctx, metadata)
+			snippet, err := v2.CreateDownstreamTlsContext(ctx, metadata)
 			// then
 			Expect(err).ToNot(HaveOccurred())
 			// and
@@ -76,7 +75,7 @@ var _ = Describe("CreateDownstreamTlsContext()", func() {
 				}
 
 				// when
-				snippet, err := tls.CreateDownstreamTlsContext(ctx, given.metadata)
+				snippet, err := v2.CreateDownstreamTlsContext(ctx, given.metadata)
 				// then
 				Expect(err).ToNot(HaveOccurred())
 				// when
@@ -234,7 +233,7 @@ var _ = Describe("CreateUpstreamTlsContext()", func() {
 			metadata := &core_xds.DataplaneMetadata{}
 
 			// when
-			snippet, err := tls.CreateUpstreamTlsContext(ctx, metadata, "backend", "backend")
+			snippet, err := v2.CreateUpstreamTlsContext(ctx, metadata, "backend", "backend")
 			// then
 			Expect(err).ToNot(HaveOccurred())
 			// and
@@ -281,7 +280,7 @@ var _ = Describe("CreateUpstreamTlsContext()", func() {
 				}
 
 				// when
-				snippet, err := tls.CreateUpstreamTlsContext(ctx, given.metadata, given.upstreamService, "")
+				snippet, err := v2.CreateUpstreamTlsContext(ctx, given.metadata, given.upstreamService, "")
 				// then
 				Expect(err).ToNot(HaveOccurred())
 				// when
diff --git a/pkg/xds/envoy/tls/v3/tls_suite_test.go b/pkg/xds/envoy/tls/v3/tls_suite_test.go
new file mode 100644
index 000000000..9308f9091
--- /dev/null
+++ b/pkg/xds/envoy/tls/v3/tls_suite_test.go
@@ -0,0 +1,18 @@
+package v3_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	util_test "github.com/kumahq/kuma/pkg/util/test"
+)
+
+func TestTLS(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Envoy TLS Suite",
+		[]Reporter{util_test.NewlineReporter{}})
+}
diff --git a/pkg/xds/envoy/tls/v3/tls_test.go b/pkg/xds/envoy/tls/v3/tls_test.go
new file mode 100644
index 000000000..37ff3a07a
--- /dev/null
+++ b/pkg/xds/envoy/tls/v3/tls_test.go
@@ -0,0 +1,424 @@
+package v3_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	xds_context "github.com/kumahq/kuma/pkg/xds/context"
+	v3 "github.com/kumahq/kuma/pkg/xds/envoy/tls/v3"
+
+	util_proto "github.com/kumahq/kuma/pkg/util/proto"
+)
+
+var _ = Describe("CreateDownstreamTlsContext()", func() {
+
+	Context("when mTLS is disabled on a given Mesh", func() {
+
+		It("should return `nil`", func() {
+			// given
+			ctx := xds_context.Context{
+				Mesh: xds_context.MeshContext{
+					Resource: mesh_core.NewMeshResource(),
+				},
+			}
+			metadata := &core_xds.DataplaneMetadata{}
+
+			// when
+			snippet, err := v3.CreateDownstreamTlsContext(ctx, metadata)
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(snippet).To(BeNil())
+		})
+	})
+
+	Context("when mTLS is enabled on a given Mesh", func() {
+
+		type testCase struct {
+			metadata *core_xds.DataplaneMetadata
+			expected string
+		}
+
+		DescribeTable("should generate proper Envoy config",
+			func(given testCase) {
+				// given
+				ctx := xds_context.Context{
+					ConnectionInfo: xds_context.ConnectionInfo{
+						Authority: "kuma-control-plane:5677",
+					},
+					ControlPlane: &xds_context.ControlPlaneContext{
+						SdsTlsCert: []byte("CERTIFICATE"),
+					},
+					Mesh: xds_context.MeshContext{
+						Resource: &mesh_core.MeshResource{
+							Meta: &test_model.ResourceMeta{
+								Name: "default",
+							},
+							Spec: &mesh_proto.Mesh{
+								Mtls: &mesh_proto.Mesh_Mtls{
+									EnabledBackend: "builtin",
+									Backends: []*mesh_proto.CertificateAuthorityBackend{
+										{
+											Name: "builtin",
+											Type: "builtin",
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+
+				// when
+				snippet, err := v3.CreateDownstreamTlsContext(ctx, given.metadata)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// when
+				actual, err := util_proto.ToYAML(snippet)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// and
+				Expect(actual).To(MatchYAML(given.expected))
+			},
+			Entry("metadata is `nil`", testCase{
+				metadata: nil,
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - prefix: spiffe://default/
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                requireClientCertificate: true
+`,
+			}),
+			Entry("dataplane without a token", testCase{
+				metadata: &core_xds.DataplaneMetadata{},
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - prefix: spiffe://default/
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                requireClientCertificate: true
+`,
+			}),
+			Entry("dataplane with a token", testCase{
+
+				metadata: &core_xds.DataplaneMetadata{
+					DataplaneTokenPath: "/path/to/token",
+				},
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - prefix: spiffe://default/
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              callCredentials:
+                              - fromPlugin:
+                                  name: envoy.grpc_credentials.file_based_metadata
+                                  typedConfig:
+                                    '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                    secretData:
+                                      filename: /path/to/token
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            callCredentials:
+                            - fromPlugin:
+                                name: envoy.grpc_credentials.file_based_metadata
+                                typedConfig:
+                                  '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                  secretData:
+                                    filename: /path/to/token
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+                requireClientCertificate: true
+`,
+			}),
+		)
+	})
+})
+
+var _ = Describe("CreateUpstreamTlsContext()", func() {
+
+	Context("when mTLS is disabled on a given Mesh", func() {
+
+		It("should return `nil`", func() {
+			// given
+			ctx := xds_context.Context{
+				Mesh: xds_context.MeshContext{
+					Resource: mesh_core.NewMeshResource(),
+				},
+			}
+			metadata := &core_xds.DataplaneMetadata{}
+
+			// when
+			snippet, err := v3.CreateUpstreamTlsContext(ctx, metadata, "backend", "backend")
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			// and
+			Expect(snippet).To(BeNil())
+		})
+	})
+
+	Context("when mTLS is enabled on a given Mesh", func() {
+
+		type testCase struct {
+			metadata        *core_xds.DataplaneMetadata
+			upstreamService string
+			expected        string
+		}
+
+		DescribeTable("should generate proper Envoy config",
+			func(given testCase) {
+				// given
+				ctx := xds_context.Context{
+					ConnectionInfo: xds_context.ConnectionInfo{
+						Authority: "kuma-control-plane:5677",
+					},
+					ControlPlane: &xds_context.ControlPlaneContext{
+						SdsTlsCert: []byte("CERTIFICATE"),
+					},
+					Mesh: xds_context.MeshContext{
+						Resource: &mesh_core.MeshResource{
+							Meta: &test_model.ResourceMeta{
+								Name: "default",
+							},
+							Spec: &mesh_proto.Mesh{
+								Mtls: &mesh_proto.Mesh_Mtls{
+									EnabledBackend: "builtin",
+									Backends: []*mesh_proto.CertificateAuthorityBackend{
+										{
+											Name: "builtin",
+											Type: "builtin",
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+
+				// when
+				snippet, err := v3.CreateUpstreamTlsContext(ctx, given.metadata, given.upstreamService, "")
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// when
+				actual, err := util_proto.ToYAML(snippet)
+				// then
+				Expect(err).ToNot(HaveOccurred())
+				// and
+				Expect(actual).To(MatchYAML(given.expected))
+			},
+			Entry("metadata is `nil`", testCase{
+				metadata:        nil,
+				upstreamService: "backend",
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+`,
+			}),
+			Entry("dataplane without a token", testCase{
+				metadata:        &core_xds.DataplaneMetadata{},
+				upstreamService: "backend",
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+`,
+			}),
+			Entry("dataplane with a token", testCase{
+				upstreamService: "backend",
+				metadata: &core_xds.DataplaneMetadata{
+					DataplaneTokenPath: "/path/to/token",
+				},
+				expected: `
+                commonTlsContext:
+                  combinedValidationContext:
+                    defaultValidationContext:
+                      matchSubjectAltNames:
+                      - exact: spiffe://default/backend
+                    validationContextSdsSecretConfig:
+                      name: mesh_ca
+                      sdsConfig:
+                        apiConfigSource:
+                          apiType: GRPC
+                          grpcServices:
+                          - googleGrpc:
+                              callCredentials:
+                              - fromPlugin:
+                                  name: envoy.grpc_credentials.file_based_metadata
+                                  typedConfig:
+                                    '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                    secretData:
+                                      filename: /path/to/token
+                              channelCredentials:
+                                sslCredentials:
+                                  rootCerts:
+                                    inlineBytes: Q0VSVElGSUNBVEU=
+                              credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                              statPrefix: sds_mesh_ca
+                              targetUri: kuma-control-plane:5677
+                  tlsCertificateSdsSecretConfigs:
+                  - name: identity_cert
+                    sdsConfig:
+                      apiConfigSource:
+                        apiType: GRPC
+                        grpcServices:
+                        - googleGrpc:
+                            callCredentials:
+                            - fromPlugin:
+                                name: envoy.grpc_credentials.file_based_metadata
+                                typedConfig:
+                                  '@type': type.googleapis.com/envoy.config.grpc_credential.v3.FileBasedMetadataConfig
+                                  secretData:
+                                    filename: /path/to/token
+                            channelCredentials:
+                              sslCredentials:
+                                rootCerts:
+                                  inlineBytes: Q0VSVElGSUNBVEU=
+                            credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+                            statPrefix: sds_identity_cert
+                            targetUri: kuma-control-plane:5677
+`,
+			}),
+		)
+	})
+})
diff --git a/pkg/xds/generator/proxy_template_profile_source_test.go b/pkg/xds/generator/proxy_template_profile_source_test.go
index 5d572856b..dd32be82c 100644
--- a/pkg/xds/generator/proxy_template_profile_source_test.go
+++ b/pkg/xds/generator/proxy_template_profile_source_test.go
@@ -8,7 +8,7 @@ import (
 
 	envoy_api_v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 
-	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints"
+	"github.com/kumahq/kuma/pkg/xds/envoy/endpoints/v2"
 
 	"github.com/golang/protobuf/ptypes"
 	. "github.com/onsi/ginkgo"