Repository: kumahq/kuma
Pull request #1040 (2020-09-24 14:32:10)

feat(kuma-cp) auth on XDS
### Summary

This PR introduces authorization on XDS. Therefore you can only create DP with a new DP lifecycle if you've got proper dataplane token.

Some of the Auth code was copied from the SDS auth. The next step would be to get rid of the SDS and server certs through XDS via ADS.

### Documentation

- [x] Transparent for the users. Flow is the same, but with more security.



Patch:
diff --git a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
index 433d993ba..24b01972b 100644
--- a/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
+++ b/app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
@@ -10,6 +10,9 @@ import (
 	"strings"
 
 	envoy_bootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
+	// import only limited proto definitions from Go Control Plane to save kuma-dp size
+	_ "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v2alpha"
+	_ "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3"
 	"github.com/golang/protobuf/proto"
 	"github.com/pkg/errors"
 	"github.com/sethvargo/go-retry"
diff --git a/pkg/core/bootstrap/autoconfig.go b/pkg/core/bootstrap/autoconfig.go
index 5821e19f1..731097ae7 100644
--- a/pkg/core/bootstrap/autoconfig.go
+++ b/pkg/core/bootstrap/autoconfig.go
@@ -28,11 +28,10 @@ func autoconfigure(cfg *kuma_cp.Config) error {
 }
 
 func autoconfigureServersTLS(cfg *kuma_cp.Config) {
-	//if cfg.XdsServer.TlsCertFile == "" {
-	//	cfg.XdsServer.TlsCertFile = cfg.General.TlsCertFile
-	//	cfg.XdsServer.TlsKeyFile = cfg.General.TlsKeyFile
-	//}
-	// todo restore in the next PR
+	if cfg.XdsServer.TlsCertFile == "" {
+		cfg.XdsServer.TlsCertFile = cfg.General.TlsCertFile
+		cfg.XdsServer.TlsKeyFile = cfg.General.TlsKeyFile
+	}
 	if cfg.BootstrapServer.TlsCertFile == "" {
 		cfg.BootstrapServer.TlsCertFile = cfg.General.TlsCertFile
 		cfg.BootstrapServer.TlsKeyFile = cfg.General.TlsKeyFile
diff --git a/pkg/sds/auth/universal/authenticator.go b/pkg/sds/auth/universal/authenticator.go
index c246d552b..d5a7697ca 100644
--- a/pkg/sds/auth/universal/authenticator.go
+++ b/pkg/sds/auth/universal/authenticator.go
@@ -46,7 +46,7 @@ func (u *universalAuthenticator) Authenticate(ctx context.Context, proxyId core_
 }
 
 func (u *universalAuthenticator) reviewToken(dataplane *mesh.DataplaneResource, credential sds_auth.Credential) error {
-	dpIdentity, err := u.issuer.Validate(credential)
+	dpIdentity, err := u.issuer.Validate(builtin_issuer.Token(credential))
 	if err != nil {
 		return err
 	}
diff --git a/pkg/tokens/builtin/issuer/issuer.go b/pkg/tokens/builtin/issuer/issuer.go
index 5bc6d3f72..9e0eb1871 100644
--- a/pkg/tokens/builtin/issuer/issuer.go
+++ b/pkg/tokens/builtin/issuer/issuer.go
@@ -5,9 +5,10 @@ import (
 	"github.com/pkg/errors"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
-	"github.com/kumahq/kuma/pkg/sds/auth"
 )
 
+type Token = string
+
 type DataplaneIdentity struct {
 	Name string
 	Mesh string
@@ -18,8 +19,8 @@ type DataplaneIdentity struct {
 // Issued token can be bound by name, mesh or tags so you can pick your level of security.
 // See pkg/sds/auth/universal/authenticator.go to check algorithm for authentication
 type DataplaneTokenIssuer interface {
-	Generate(identity DataplaneIdentity) (auth.Credential, error)
-	Validate(credential auth.Credential) (DataplaneIdentity, error)
+	Generate(identity DataplaneIdentity) (Token, error)
+	Validate(token Token) (DataplaneIdentity, error)
 }
 
 type claims struct {
@@ -52,7 +53,7 @@ func (i *jwtTokenIssuer) signingKey() ([]byte, error) {
 	return signingKey, nil
 }
 
-func (i *jwtTokenIssuer) Generate(identity DataplaneIdentity) (auth.Credential, error) {
+func (i *jwtTokenIssuer) Generate(identity DataplaneIdentity) (Token, error) {
 	signingKey, err := i.signingKey()
 	if err != nil {
 		return "", err
@@ -75,10 +76,10 @@ func (i *jwtTokenIssuer) Generate(identity DataplaneIdentity) (auth.Credential,
 	if err != nil {
 		return "", errors.Wrap(err, "could not sign a token")
 	}
-	return auth.Credential(tokenString), nil
+	return tokenString, nil
 }
 
-func (i *jwtTokenIssuer) Validate(credential auth.Credential) (DataplaneIdentity, error) {
+func (i *jwtTokenIssuer) Validate(rawToken Token) (DataplaneIdentity, error) {
 	signingKey, err := i.signingKey()
 	if err != nil {
 		return DataplaneIdentity{}, err
@@ -86,7 +87,7 @@ func (i *jwtTokenIssuer) Validate(credential auth.Credential) (DataplaneIdentity
 
 	c := &claims{}
 
-	token, err := jwt.ParseWithClaims(string(credential), c, func(*jwt.Token) (interface{}, error) {
+	token, err := jwt.ParseWithClaims(rawToken, c, func(*jwt.Token) (interface{}, error) {
 		return signingKey, nil
 	})
 	if err != nil {
diff --git a/pkg/util/xds/callbacks_chain.go b/pkg/util/xds/callbacks_chain.go
index eacef90c0..26f73f3a4 100644
--- a/pkg/util/xds/callbacks_chain.go
+++ b/pkg/util/xds/callbacks_chain.go
@@ -5,8 +5,6 @@ import (
 
 	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
 	envoy_xds "github.com/envoyproxy/go-control-plane/pkg/server/v2"
-
-	"go.uber.org/multierr"
 )
 
 type CallbacksChain []envoy_xds.Callbacks
@@ -15,11 +13,13 @@ var _ envoy_xds.Callbacks = CallbacksChain{}
 
 // OnStreamOpen is called once an xDS stream is open with a stream ID and the type URL (or "" for ADS).
 // Returning an error will end processing and close the stream. OnStreamClosed will still be called.
-func (chain CallbacksChain) OnStreamOpen(ctx context.Context, streamID int64, typ string) (errs error) {
+func (chain CallbacksChain) OnStreamOpen(ctx context.Context, streamID int64, typ string) error {
 	for _, cb := range chain {
-		errs = multierr.Append(errs, cb.OnStreamOpen(ctx, streamID, typ))
+		if err := cb.OnStreamOpen(ctx, streamID, typ); err != nil {
+			return err
+		}
 	}
-	return
+	return nil
 }
 
 // OnStreamClosed is called immediately prior to closing an xDS stream with a stream ID.
@@ -32,11 +32,13 @@ func (chain CallbacksChain) OnStreamClosed(streamID int64) {
 
 // OnStreamRequest is called once a request is received on a stream.
 // Returning an error will end processing and close the stream. OnStreamClosed will still be called.
-func (chain CallbacksChain) OnStreamRequest(streamID int64, req *envoy.DiscoveryRequest) (errs error) {
+func (chain CallbacksChain) OnStreamRequest(streamID int64, req *envoy.DiscoveryRequest) error {
 	for _, cb := range chain {
-		errs = multierr.Append(errs, cb.OnStreamRequest(streamID, req))
+		if err := cb.OnStreamRequest(streamID, req); err != nil {
+			return err
+		}
 	}
-	return
+	return nil
 }
 
 // OnStreamResponse is called immediately prior to sending a response on a stream.
@@ -49,11 +51,13 @@ func (chain CallbacksChain) OnStreamResponse(streamID int64, req *envoy.Discover
 
 // OnFetchRequest is called for each Fetch request. Returning an error will end processing of the
 // request and respond with an error.
-func (chain CallbacksChain) OnFetchRequest(ctx context.Context, req *envoy.DiscoveryRequest) (errs error) {
+func (chain CallbacksChain) OnFetchRequest(ctx context.Context, req *envoy.DiscoveryRequest) error {
 	for _, cb := range chain {
-		errs = multierr.Append(errs, cb.OnFetchRequest(ctx, req))
+		if err := cb.OnFetchRequest(ctx, req); err != nil {
+			return err
+		}
 	}
-	return
+	return nil
 }
 
 // OnFetchRequest is called for each Fetch request. Returning an error will end processing of the
diff --git a/pkg/xds/auth/callbacks.go b/pkg/xds/auth/callbacks.go
new file mode 100644
index 000000000..0b7e0fd41
--- /dev/null
+++ b/pkg/xds/auth/callbacks.go
@@ -0,0 +1,156 @@
+package auth
+
+import (
+	"context"
+	"sync"
+
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_server "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	"github.com/pkg/errors"
+	"google.golang.org/grpc/metadata"
+
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_manager "github.com/kumahq/kuma/pkg/core/resources/manager"
+	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
+	core_xds "github.com/kumahq/kuma/pkg/core/xds"
+)
+
+const authorization = "authorization"
+
+func NewCallbacks(resManager core_manager.ResourceManager, authenticator Authenticator) envoy_server.Callbacks {
+	return &authCallbacks{
+		resManager:    resManager,
+		authenticator: authenticator,
+		contexts:      map[core_xds.StreamID]context.Context{},
+		authenticated: map[core_xds.StreamID]string{},
+	}
+}
+
+// authCallback checks if the DiscoveryRequest is authorized, ie. if it has a valid Dataplane Token/Service Account Token.
+type authCallbacks struct {
+	resManager    core_manager.ResourceManager
+	authenticator Authenticator
+
+	sync.RWMutex // protects contexts and authenticated
+	// contexts stores context for every stream, since Context from which we can extract auth data is only available in OnStreamOpen
+	contexts map[core_xds.StreamID]context.Context
+	// authenticated stores authenticated ProxyID for stream. We don't want to authenticate every because since on K8S we execute ReviewToken which is expensive
+	// as long as client won't change ProxyID it's safe to authenticate only once.
+	authenticated map[core_xds.StreamID]string
+}
+
+var _ envoy_server.Callbacks = &authCallbacks{}
+
+func (a *authCallbacks) OnStreamOpen(ctx context.Context, streamID core_xds.StreamID, _ string) error {
+	a.Lock()
+	defer a.Unlock()
+
+	a.contexts[streamID] = ctx
+	return nil
+}
+
+func (a *authCallbacks) OnStreamClosed(streamID core_xds.StreamID) {
+	a.Lock()
+	delete(a.contexts, streamID)
+	delete(a.authenticated, streamID)
+	a.Unlock()
+}
+
+func (a *authCallbacks) OnStreamRequest(streamID core_xds.StreamID, req *envoy_api.DiscoveryRequest) error {
+	if id, alreadyAuthenticated := a.authNodeId(streamID); alreadyAuthenticated {
+		if req.Node != nil && req.Node.Id != id {
+			return errors.Errorf("stream was authenticated for ID %s. Received request is for node with ID %s. Node ID cannot be changed after stream is initialized", id, req.Node.Id)
+		}
+		return nil
+	}
+
+	credential, err := a.credential(streamID)
+	if err != nil {
+		return err
+	}
+	err = a.authenticate(credential, req)
+	if err != nil {
+		return err
+	}
+	a.Lock()
+	a.authenticated[streamID] = req.Node.Id
+	a.Unlock()
+	return nil
+}
+
+func (a *authCallbacks) authNodeId(streamID core_xds.StreamID) (string, bool) {
+	a.RLock()
+	defer a.RUnlock()
+	id, ok := a.authenticated[streamID]
+	return id, ok
+}
+
+func (a *authCallbacks) credential(streamID core_xds.StreamID) (Credential, error) {
+	a.RLock()
+	defer a.RUnlock()
+
+	ctx, exists := a.contexts[streamID]
+	if !exists {
+		return "", errors.Errorf("there is no context for stream ID %d", streamID)
+	}
+	credential, err := extractCredential(ctx)
+	if err != nil {
+		return "", errors.Wrap(err, "could not extract credential from DiscoveryRequest")
+	}
+	return credential, err
+}
+
+func (a *authCallbacks) authenticate(credential Credential, req *envoy_api.DiscoveryRequest) error {
+	dataplane := &core_mesh.DataplaneResource{}
+	md := core_xds.DataplaneMetadataFromNode(req.Node)
+	if md.DataplaneResource != nil {
+		dataplane = md.DataplaneResource
+	} else {
+		proxyId, err := core_xds.ParseProxyId(req.Node)
+		if err != nil {
+			return errors.Wrap(err, "SDS request must have a valid Proxy Id")
+		}
+		err = a.resManager.Get(context.Background(), dataplane, core_store.GetByKey(proxyId.Name, proxyId.Mesh))
+		if err != nil {
+			if core_store.IsResourceNotFound(err) {
+				return errors.New("dataplane not found. Create Dataplane in Kuma CP first or pass it as an argument to kuma-dp")
+			}
+			return err
+		}
+	}
+
+	if err := a.authenticator.Authenticate(context.Background(), dataplane, credential); err != nil {
+		return errors.Wrap(err, "authentication failed")
+	}
+	return nil
+}
+
+func (a *authCallbacks) OnStreamResponse(core_xds.StreamID, *envoy_api.DiscoveryRequest, *envoy_api.DiscoveryResponse) {
+}
+
+func (a *authCallbacks) OnFetchRequest(ctx context.Context, request *envoy_api.DiscoveryRequest) error {
+	credential, err := extractCredential(ctx)
+	if err != nil {
+		return errors.Wrap(err, "could not extract credential from DiscoveryRequest")
+	}
+	return a.authenticate(credential, request)
+}
+
+func (a *authCallbacks) OnFetchResponse(*envoy_api.DiscoveryRequest, *envoy_api.DiscoveryResponse) {
+}
+
+var _ envoy_server.Callbacks = &authCallbacks{}
+
+func extractCredential(ctx context.Context) (Credential, error) {
+	metadata, ok := metadata.FromIncomingContext(ctx)
+	if !ok {
+		return "", errors.Errorf("request has no metadata")
+	}
+	if values, ok := metadata[authorization]; ok {
+		if len(values) != 1 {
+			return "", errors.Errorf("request must have exactly 1 %q header, got %d", authorization, len(values))
+		}
+		return values[0], nil
+	}
+	return "", nil
+}
diff --git a/pkg/xds/auth/interfaces.go b/pkg/xds/auth/interfaces.go
new file mode 100644
index 000000000..6fdf50ee4
--- /dev/null
+++ b/pkg/xds/auth/interfaces.go
@@ -0,0 +1,13 @@
+package auth
+
+import (
+	"context"
+
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+)
+
+type Credential = string
+
+type Authenticator interface {
+	Authenticate(ctx context.Context, dataplane *core_mesh.DataplaneResource, credential Credential) error
+}
diff --git a/pkg/xds/auth/k8s/authenticator.go b/pkg/xds/auth/k8s/authenticator.go
new file mode 100644
index 000000000..179592068
--- /dev/null
+++ b/pkg/xds/auth/k8s/authenticator.go
@@ -0,0 +1,58 @@
+package k8s
+
+import (
+	"context"
+	"strings"
+
+	"github.com/pkg/errors"
+
+	kube_auth "k8s.io/api/authentication/v1"
+	kube_client "sigs.k8s.io/controller-runtime/pkg/client"
+
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	util_k8s "github.com/kumahq/kuma/pkg/util/k8s"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+)
+
+func New(client kube_client.Client) auth.Authenticator {
+	return &kubeAuthenticator{
+		client: client,
+	}
+}
+
+type kubeAuthenticator struct {
+	client kube_client.Client
+}
+
+func (k *kubeAuthenticator) Authenticate(ctx context.Context, dataplane *core_mesh.DataplaneResource, credential auth.Credential) error {
+	if credential == "" {
+		return errors.New("authentication failed: k8s token is missing")
+	}
+	tokenReview := &kube_auth.TokenReview{
+		Spec: kube_auth.TokenReviewSpec{
+			Token: credential,
+		},
+	}
+	if err := k.client.Create(ctx, tokenReview); err != nil {
+		return errors.Wrap(err, "authentication failed: call to TokenReview API failed")
+	}
+	if !tokenReview.Status.Authenticated {
+		return errors.Errorf("authentication failed: token doesn't belong to a valid user")
+	}
+	userInfo := strings.Split(tokenReview.Status.User.Username, ":")
+	if len(userInfo) != 4 {
+		return errors.Errorf("authentication failed: username inside TokenReview response has unexpected format: %q", tokenReview.Status.User.Username)
+	}
+	if !(userInfo[0] == "system" && userInfo[1] == "serviceaccount") {
+		return errors.Errorf("authentication failed: token must belong to a k8s system account, got %q", tokenReview.Status.User.Username)
+	}
+	_, proxyNamespace, err := util_k8s.CoreNameToK8sName(dataplane.Meta.GetName())
+	if err != nil {
+		return err
+	}
+	namespace := userInfo[2]
+	if namespace != proxyNamespace {
+		return errors.Errorf("authentication failed: token belongs to a namespace (%q) different from proxyId (%q)", namespace, proxyNamespace)
+	}
+	return nil
+}
diff --git a/pkg/xds/auth/universal/authenticator.go b/pkg/xds/auth/universal/authenticator.go
new file mode 100644
index 000000000..9619d37ed
--- /dev/null
+++ b/pkg/xds/auth/universal/authenticator.go
@@ -0,0 +1,64 @@
+package universal
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	builtin_issuer "github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+)
+
+func NewAuthenticator(issuer builtin_issuer.DataplaneTokenIssuer) auth.Authenticator {
+	return &universalAuthenticator{
+		issuer: issuer,
+	}
+}
+
+// universalAuthenticator defines authentication for Dataplane Tokens
+// All fields in token are optional, so we only validate data that is available in token. This way you can pick your level of security.
+// Generate token for mesh+name for maximum security.
+// Generate token for mesh+tags(ex. kuma.io/service) so you can reuse the token for many instances of the same service.
+// Generate token for mesh if you trust the scope of the mesh.
+//
+// If you generate token bound to tags all tags values have to match the dataplane, so for example if you have a Dataplane
+// with inbounds: 1) kuma.io/service:web 2) kuma.io/service:web-api, you need token for both values kuma.io/service=web,web-api
+// Dataplane also needs to have all tags defined in the token
+type universalAuthenticator struct {
+	issuer builtin_issuer.DataplaneTokenIssuer
+}
+
+func (u *universalAuthenticator) Authenticate(ctx context.Context, dataplane *core_mesh.DataplaneResource, credential auth.Credential) error {
+	dpIdentity, err := u.issuer.Validate(credential)
+	if err != nil {
+		return err
+	}
+
+	if dpIdentity.Name != "" && dataplane.Meta.GetName() != dpIdentity.Name {
+		return errors.Errorf("proxy name from requestor: %s is different than in token: %s", dataplane.Meta.GetName(), dpIdentity.Name)
+	}
+	if dpIdentity.Mesh != "" && dataplane.Meta.GetMesh() != dpIdentity.Mesh {
+		return errors.Errorf("proxy mesh from requestor: %s is different than in token: %s", dataplane.Meta.GetMesh(), dpIdentity.Mesh)
+	}
+	if err := validateTags(dpIdentity.Tags, dataplane.Spec.Tags()); err != nil {
+		return err
+	}
+	return nil
+}
+
+func validateTags(tokenTags mesh_proto.MultiValueTagSet, dpTags mesh_proto.MultiValueTagSet) error {
+	for tagName, allowedValues := range tokenTags {
+		dpValues, exist := dpTags[tagName]
+		if !exist {
+			return errors.Errorf("dataplane has no tag %q required by the token", tagName)
+		}
+		for value := range dpValues {
+			if !allowedValues[value] {
+				return errors.Errorf("dataplane contains tag %q with value %q which is not allowed with this token. Allowed values in token are %q", tagName, value, tokenTags.Values(tagName))
+			}
+		}
+	}
+	return nil
+}
diff --git a/pkg/xds/auth/universal/noop_authenticator.go b/pkg/xds/auth/universal/noop_authenticator.go
new file mode 100644
index 000000000..31d4e77da
--- /dev/null
+++ b/pkg/xds/auth/universal/noop_authenticator.go
@@ -0,0 +1,19 @@
+package universal
+
+import (
+	"context"
+
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+)
+
+func NewNoopAuthenticator() auth.Authenticator {
+	return &noopAuthenticator{}
+}
+
+type noopAuthenticator struct {
+}
+
+func (u *noopAuthenticator) Authenticate(ctx context.Context, dataplane *core_mesh.DataplaneResource, _ auth.Credential) error {
+	return nil
+}
diff --git a/pkg/xds/bootstrap/generator.go b/pkg/xds/bootstrap/generator.go
index 2042f3b76..51513a36a 100644
--- a/pkg/xds/bootstrap/generator.go
+++ b/pkg/xds/bootstrap/generator.go
@@ -22,6 +22,7 @@ import (
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	util_proto "github.com/kumahq/kuma/pkg/util/proto"
 	"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
+	_ "github.com/kumahq/kuma/pkg/xds/envoy" // import Envoy protobuf definitions so (un)marshalling Envoy protobuf works in tests (normally it is imported in root.go)
 )
 
 type BootstrapGenerator interface {
diff --git a/pkg/xds/bootstrap/template.go b/pkg/xds/bootstrap/template.go
index 4c760709e..51c6c83a7 100644
--- a/pkg/xds/bootstrap/template.go
+++ b/pkg/xds/bootstrap/template.go
@@ -58,43 +58,29 @@ dynamic_resources:
   cds_config: {ads: {}}
   ads_config:
     api_type: GRPC
+    timeout: {{ .XdsConnectTimeout }}
     grpc_services:
-    - envoy_grpc:
-        cluster_name: ads_cluster
-
+    - googleGrpc:
+{{ if .DataplaneTokenPath }}
+        callCredentials:
+        - fromPlugin:
+            name: envoy.grpc_credentials.file_based_metadata
+            typedConfig:
+              '@type': type.googleapis.com/envoy.config.grpc_credential.v2alpha.FileBasedMetadataConfig
+              secretData:
+                filename: {{ .DataplaneTokenPath }}
+        credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+{{ end }}
+{{ if .CertBytes}}
+        channelCredentials:
+          sslCredentials:
+            rootCerts:
+              inlineBytes: {{ .CertBytes }}
+{{ end }}
+        statPrefix: ads
+        targetUri: {{ .XdsHost }}:{{ .XdsPort }}
 static_resources:
   clusters:
-  - name: ads_cluster
-    connect_timeout: {{ .XdsConnectTimeout }}
-    type: STRICT_DNS
-    lb_policy: ROUND_ROBIN
-    http2_protocol_options: {}
-    upstream_connection_options:
-      # configure a TCP keep-alive to detect and reconnect to the admin
-      # server in the event of a TCP socket half open connection
-      tcp_keepalive: {}
-    load_assignment:
-      cluster_name: ads_cluster
-      endpoints:
-      - lb_endpoints:
-        - endpoint:
-            address:
-              socket_address:
-                address: {{ .XdsHost }}
-                port_value: {{ .XdsPort }}
-{{ if .CertBytes }}
-    transport_socket:
-      name: envoy.transport_sockets.tls
-      typed_config:
-        "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext
-        sni: {{ .XdsHost }}
-        common_tls_context:
-          tls_params:
-            tls_minimum_protocol_version: TLSv1_2
-          validation_context:
-            trusted_ca:
-              inline_bytes: "{{ .CertBytes }}"
-{{ end }}
   - name: access_log_sink
     connect_timeout: {{ .XdsConnectTimeout }}
     type: STATIC
diff --git a/pkg/xds/server/components.go b/pkg/xds/server/components.go
index 20ce9ea2f..870fb471e 100644
--- a/pkg/xds/server/components.go
+++ b/pkg/xds/server/components.go
@@ -5,9 +5,12 @@ import (
 	"time"
 
 	envoy_xds "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	"github.com/pkg/errors"
 	"github.com/prometheus/client_golang/prometheus"
+	kube_auth "k8s.io/api/authentication/v1"
 
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	config_core "github.com/kumahq/kuma/pkg/config/core"
 	"github.com/kumahq/kuma/pkg/core"
 	"github.com/kumahq/kuma/pkg/core/faultinjections"
 	"github.com/kumahq/kuma/pkg/core/logs"
@@ -18,8 +21,13 @@ import (
 	core_runtime "github.com/kumahq/kuma/pkg/core/runtime"
 	"github.com/kumahq/kuma/pkg/core/xds"
 	"github.com/kumahq/kuma/pkg/metrics"
+	k8s_runtime "github.com/kumahq/kuma/pkg/runtime/k8s"
+	"github.com/kumahq/kuma/pkg/tokens/builtin"
 	util_watchdog "github.com/kumahq/kuma/pkg/util/watchdog"
 	util_xds "github.com/kumahq/kuma/pkg/util/xds"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+	k8s_auth "github.com/kumahq/kuma/pkg/xds/auth/k8s"
+	universal_auth "github.com/kumahq/kuma/pkg/xds/auth/universal"
 	xds_bootstrap "github.com/kumahq/kuma/pkg/xds/bootstrap"
 	xds_context "github.com/kumahq/kuma/pkg/xds/context"
 	"github.com/kumahq/kuma/pkg/xds/ingress"
@@ -35,6 +43,12 @@ var (
 func SetupServer(rt core_runtime.Runtime) error {
 	reconciler := DefaultReconciler(rt)
 
+	authenticator, err := DefaultAuthenticator(rt)
+	if err != nil {
+		return err
+	}
+	authCallbacks := auth.NewCallbacks(rt.ResourceManager(), authenticator)
+
 	metadataTracker := NewDataplaneMetadataTracker()
 	lifecycle := NewDataplaneLifecycle(rt.ResourceManager())
 
@@ -55,6 +69,7 @@ func SetupServer(rt core_runtime.Runtime) error {
 	}
 	callbacks := util_xds.CallbacksChain{
 		statsCallbacks,
+		authCallbacks,
 		syncTracker,
 		metadataTracker,
 		lifecycle,
@@ -80,6 +95,39 @@ func SetupServer(rt core_runtime.Runtime) error {
 	)
 }
 
+func NewKubeAuthenticator(rt core_runtime.Runtime) (auth.Authenticator, error) {
+	mgr, ok := k8s_runtime.FromManagerContext(rt.Extensions())
+	if !ok {
+		return nil, errors.Errorf("k8s controller runtime Manager hasn't been configured")
+	}
+	if err := kube_auth.AddToScheme(mgr.GetScheme()); err != nil {
+		return nil, errors.Wrapf(err, "could not add %q to scheme", kube_auth.SchemeGroupVersion)
+	}
+	return k8s_auth.New(mgr.GetClient()), nil
+}
+
+func NewUniversalAuthenticator(rt core_runtime.Runtime) (auth.Authenticator, error) {
+	if !rt.Config().AdminServer.Apis.DataplaneToken.Enabled {
+		return universal_auth.NewNoopAuthenticator(), nil
+	}
+	issuer, err := builtin.NewDataplaneTokenIssuer(rt)
+	if err != nil {
+		return nil, err
+	}
+	return universal_auth.NewAuthenticator(issuer), nil
+}
+
+func DefaultAuthenticator(rt core_runtime.Runtime) (auth.Authenticator, error) {
+	switch env := rt.Config().Environment; env {
+	case config_core.KubernetesEnvironment:
+		return NewKubeAuthenticator(rt)
+	case config_core.UniversalEnvironment:
+		return NewUniversalAuthenticator(rt)
+	default:
+		return nil, errors.Errorf("unable to choose SDS authenticator for environment type %q", env)
+	}
+}
+
 func DefaultReconciler(rt core_runtime.Runtime) SnapshotReconciler {
 	return &reconciler{
 		&templateSnapshotGenerator{


Test patch:
diff --git a/app/kumactl/pkg/tokens/client_test.go b/app/kumactl/pkg/tokens/client_test.go
index 3755a4313..89969e389 100644
--- a/app/kumactl/pkg/tokens/client_test.go
+++ b/app/kumactl/pkg/tokens/client_test.go
@@ -16,7 +16,6 @@ import (
 	admin_server_config "github.com/kumahq/kuma/pkg/config/admin-server"
 	config_kumactl "github.com/kumahq/kuma/pkg/config/app/kumactl/v1alpha1"
 	"github.com/kumahq/kuma/pkg/metrics"
-	"github.com/kumahq/kuma/pkg/sds/auth"
 	"github.com/kumahq/kuma/pkg/test"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/server"
@@ -27,11 +26,11 @@ type staticTokenIssuer struct {
 
 var _ issuer.DataplaneTokenIssuer = &staticTokenIssuer{}
 
-func (s *staticTokenIssuer) Generate(identity issuer.DataplaneIdentity) (auth.Credential, error) {
-	return auth.Credential(fmt.Sprintf("token-for-%s-%s", identity.Name, identity.Mesh)), nil
+func (s *staticTokenIssuer) Generate(identity issuer.DataplaneIdentity) (issuer.Token, error) {
+	return fmt.Sprintf("token-for-%s-%s", identity.Name, identity.Mesh), nil
 }
 
-func (s *staticTokenIssuer) Validate(credential auth.Credential) (issuer.DataplaneIdentity, error) {
+func (s *staticTokenIssuer) Validate(token issuer.Token) (issuer.DataplaneIdentity, error) {
 	return issuer.DataplaneIdentity{}, errors.New("not implemented")
 }
 
diff --git a/pkg/sds/auth/universal/auth_test.go b/pkg/sds/auth/universal/auth_test.go
index 3a4153ad5..e6f1c1603 100644
--- a/pkg/sds/auth/universal/auth_test.go
+++ b/pkg/sds/auth/universal/auth_test.go
@@ -76,7 +76,7 @@ var _ = Describe("Authentication flow", func() {
 			authIdentity, err := authenticator.Authenticate(context.Background(), xds.ProxyId{
 				Mesh: "default",
 				Name: "dp-1",
-			}, credential)
+			}, auth.Credential(credential))
 
 			// then
 			Expect(err).ToNot(HaveOccurred())
@@ -123,7 +123,7 @@ var _ = Describe("Authentication flow", func() {
 				Mesh: "default",
 				Name: "dp-1",
 			}
-			_, err = authenticator.Authenticate(context.Background(), authId, token)
+			_, err = authenticator.Authenticate(context.Background(), authId, auth.Credential(token))
 
 			// then
 			Expect(err).To(HaveOccurred())
@@ -205,7 +205,7 @@ var _ = Describe("Authentication flow", func() {
 		_, err = authenticator.Authenticate(context.Background(), xds.ProxyId{
 			Mesh: "default",
 			Name: "non-existent-dp",
-		}, token)
+		}, auth.Credential(token))
 
 		// then
 		Expect(err).To(MatchError(`unable to find Dataplane for proxy "default.non-existent-dp": Resource not found: type="Dataplane" name="non-existent-dp" mesh="default"`))
diff --git a/pkg/sds/server/server_test.go b/pkg/sds/server/server_test.go
index be6a60c3b..0c809550c 100644
--- a/pkg/sds/server/server_test.go
+++ b/pkg/sds/server/server_test.go
@@ -120,11 +120,12 @@ var _ = Describe("SDS Server", func() {
 		// retrieve example DP token
 		tokenIssuer, err := tokens_builtin.NewDataplaneTokenIssuer(runtime)
 		Expect(err).ToNot(HaveOccurred())
-		dpCredential, err = tokenIssuer.Generate(tokens_issuer.DataplaneIdentity{
+		token, err := tokenIssuer.Generate(tokens_issuer.DataplaneIdentity{
 			Name: dpRes.GetMeta().GetName(),
 			Mesh: dpRes.GetMeta().GetMesh(),
 		})
 		Expect(err).ToNot(HaveOccurred())
+		dpCredential = sds_auth.Credential(token)
 
 		// start the runtime
 		Expect(server.SetupServer(runtime)).To(Succeed())
diff --git a/pkg/tokens/builtin/server/webservice_test.go b/pkg/tokens/builtin/server/webservice_test.go
index 95855e4f2..fe115ac20 100644
--- a/pkg/tokens/builtin/server/webservice_test.go
+++ b/pkg/tokens/builtin/server/webservice_test.go
@@ -15,7 +15,6 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
 
-	"github.com/kumahq/kuma/pkg/sds/auth"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/server"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/server/types"
@@ -27,11 +26,11 @@ type staticTokenIssuer struct {
 
 var _ issuer.DataplaneTokenIssuer = &staticTokenIssuer{}
 
-func (s *staticTokenIssuer) Generate(identity issuer.DataplaneIdentity) (auth.Credential, error) {
-	return auth.Credential(s.resp), nil
+func (s *staticTokenIssuer) Generate(identity issuer.DataplaneIdentity) (issuer.Token, error) {
+	return s.resp, nil
 }
 
-func (s *staticTokenIssuer) Validate(credential auth.Credential) (issuer.DataplaneIdentity, error) {
+func (s *staticTokenIssuer) Validate(token issuer.Token) (issuer.DataplaneIdentity, error) {
 	return issuer.DataplaneIdentity{}, errors.New("not implemented")
 }
 
diff --git a/pkg/util/xds/callbacks_chain_test.go b/pkg/util/xds/callbacks_chain_test.go
index 330945321..df01f404c 100644
--- a/pkg/util/xds/callbacks_chain_test.go
+++ b/pkg/util/xds/callbacks_chain_test.go
@@ -61,7 +61,7 @@ var _ = Describe("CallbacksChain", func() {
 	})
 
 	Describe("OnStreamOpen", func() {
-		It("should be called sequentially and aggregate errors", func() {
+		It("should be called sequentially and return after first error", func() {
 			// given
 			ctx := context.Background()
 			streamID := int64(1)
@@ -75,10 +75,9 @@ var _ = Describe("CallbacksChain", func() {
 			// then
 			Expect(calls).To(Equal([]methodCall{
 				methodCall{"1st", "OnStreamOpen()", []interface{}{ctx, streamID, typ}},
-				methodCall{"2nd", "OnStreamOpen()", []interface{}{ctx, streamID, typ}},
 			}))
 			// and
-			Expect(err).To(MatchError("1st: OnStreamOpen(); 2nd: OnStreamOpen()"))
+			Expect(err).To(MatchError("1st: OnStreamOpen()"))
 		})
 	})
 	Describe("OnStreamClose", func() {
@@ -99,7 +98,7 @@ var _ = Describe("CallbacksChain", func() {
 		})
 	})
 	Describe("OnStreamRequest", func() {
-		It("should be called sequentially and aggregate errors", func() {
+		It("should be called sequentially and return after first error", func() {
 			// given
 			streamID := int64(1)
 			req := &envoy.DiscoveryRequest{}
@@ -113,10 +112,9 @@ var _ = Describe("CallbacksChain", func() {
 			// then
 			Expect(calls).To(Equal([]methodCall{
 				{"1st", "OnStreamRequest()", []interface{}{streamID, req}},
-				{"2nd", "OnStreamRequest()", []interface{}{streamID, req}},
 			}))
 			// and
-			Expect(err).To(MatchError("1st: OnStreamRequest(); 2nd: OnStreamRequest()"))
+			Expect(err).To(MatchError("1st: OnStreamRequest()"))
 		})
 	})
 	Describe("OnStreamResponse", func() {
diff --git a/pkg/xds/auth/auth_suite_test.go b/pkg/xds/auth/auth_suite_test.go
new file mode 100644
index 000000000..c6bf00e94
--- /dev/null
+++ b/pkg/xds/auth/auth_suite_test.go
@@ -0,0 +1,13 @@
+package auth_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestAuth(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "XDS Auth Suite")
+}
diff --git a/pkg/xds/auth/callbacks_test.go b/pkg/xds/auth/callbacks_test.go
new file mode 100644
index 000000000..0e7a349dd
--- /dev/null
+++ b/pkg/xds/auth/callbacks_test.go
@@ -0,0 +1,220 @@
+package auth_test
+
+import (
+	"context"
+	"errors"
+
+	envoy_api "github.com/envoyproxy/go-control-plane/envoy/api/v2"
+	envoy_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
+	envoy_server "github.com/envoyproxy/go-control-plane/pkg/server/v2"
+	pstruct "github.com/golang/protobuf/ptypes/struct"
+	"google.golang.org/grpc/metadata"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_manager "github.com/kumahq/kuma/pkg/core/resources/manager"
+	"github.com/kumahq/kuma/pkg/core/resources/model/rest"
+	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
+	"github.com/kumahq/kuma/pkg/plugins/resources/memory"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+type testAuthenticator struct {
+	callCounter int
+}
+
+var _ auth.Authenticator = &testAuthenticator{}
+
+func (t *testAuthenticator) Authenticate(ctx context.Context, dataplane *core_mesh.DataplaneResource, credential auth.Credential) error {
+	t.callCounter++
+	if credential == "pass" {
+		return nil
+	}
+	return errors.New("invalid credential")
+}
+
+var _ = Describe("Auth Callbacks", func() {
+
+	var testAuth *testAuthenticator
+	var resManager core_manager.ResourceManager
+	var callbacks envoy_server.Callbacks
+
+	dpRes := &core_mesh.DataplaneResource{
+		Meta: &test_model.ResourceMeta{
+			Name: "web-01",
+			Mesh: "default",
+		},
+		Spec: mesh_proto.Dataplane{
+			Networking: &mesh_proto.Dataplane_Networking{
+				Address: "127.0.0.1",
+				Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
+					{
+						Port:        8080,
+						ServicePort: 8081,
+						Tags: map[string]string{
+							"kuma.io/service":  "web",
+							"kuma.io/protocol": "http",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	BeforeEach(func() {
+		memStore := memory.NewStore()
+		resManager = core_manager.NewResourceManager(memStore)
+		testAuth = &testAuthenticator{}
+		callbacks = auth.NewCallbacks(resManager, testAuth)
+
+		err := resManager.Create(context.Background(), &core_mesh.MeshResource{}, core_store.CreateByKey("default", "default"))
+		Expect(err).ToNot(HaveOccurred())
+		err = resManager.Create(context.Background(), dpRes, core_store.CreateByKey("web-01", "default"))
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	It("should authenticate only first request on the stream", func() {
+		// given
+		ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"authorization": "pass"}))
+		streamID := int64(1)
+
+		// when
+		err := callbacks.OnStreamOpen(ctx, streamID, "")
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.web-01",
+			},
+		})
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+		Expect(testAuth.callCounter).To(Equal(1))
+
+		// when send second request that is already authenticated
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{})
+
+		// then auth is called only once
+		Expect(err).ToNot(HaveOccurred())
+		Expect(testAuth.callCounter).To(Equal(1))
+	})
+
+	It("should authenticate when DP is passed through Envoy metadata", func() {
+		// given mesh without web-01 dataplane
+		ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"authorization": "pass"}))
+		streamID := int64(1)
+		err := resManager.Delete(context.Background(), &core_mesh.DataplaneResource{}, core_store.DeleteByKey("web-01", "default"))
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		err = callbacks.OnStreamOpen(ctx, streamID, "")
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		json, err := rest.From.Resource(dpRes).MarshalJSON()
+		Expect(err).ToNot(HaveOccurred())
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.web-01",
+				Metadata: &pstruct.Struct{
+					Fields: map[string]*pstruct.Value{
+						"dataplane.resource": {
+							Kind: &pstruct.Value_StringValue{
+								StringValue: string(json),
+							},
+						},
+					},
+				},
+			},
+		})
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	It("should not let change node id once it's authenticated", func() {
+		// given
+		ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"authorization": "pass"}))
+		streamID := int64(1)
+
+		// when
+		err := callbacks.OnStreamOpen(ctx, streamID, "")
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.web-01",
+			},
+		})
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.web-02",
+			},
+		})
+
+		// then
+		Expect(err).To(MatchError("stream was authenticated for ID default.web-01. Received request is for node with ID default.web-02. Node ID cannot be changed after stream is initialized"))
+	})
+
+	It("should not authenticate when DP is absent in the CP and it's not passed through Envoy metadata", func() {
+		// given
+		ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"authorization": "pass"}))
+		streamID := int64(1)
+
+		// when
+		err := callbacks.OnStreamOpen(ctx, streamID, "")
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.web-02",
+			},
+		})
+
+		// then
+		Expect(err).To(MatchError("dataplane not found. Create Dataplane in Kuma CP first or pass it as an argument to kuma-dp"))
+	})
+
+	It("should throw an error on authentication fail", func() {
+		// given
+		ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"authorization": "invalid"}))
+		streamID := int64(1)
+
+		// when
+		err := callbacks.OnStreamOpen(ctx, streamID, "")
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+
+		// when
+		err = callbacks.OnStreamRequest(streamID, &envoy_api.DiscoveryRequest{
+			Node: &envoy_core.Node{
+				Id: "default.web-01",
+			},
+		})
+
+		// then
+		Expect(err).To(MatchError("authentication failed: invalid credential"))
+	})
+})
diff --git a/pkg/xds/auth/universal/auth_test.go b/pkg/xds/auth/universal/auth_test.go
new file mode 100644
index 000000000..3eb6dca94
--- /dev/null
+++ b/pkg/xds/auth/universal/auth_test.go
@@ -0,0 +1,190 @@
+package universal_test
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
+	"github.com/kumahq/kuma/pkg/plugins/resources/memory"
+	test_model "github.com/kumahq/kuma/pkg/test/resources/model"
+	builtin_issuer "github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+	"github.com/kumahq/kuma/pkg/xds/auth/universal"
+)
+
+var _ = Describe("Authentication flow", func() {
+	var privateKey = []byte("testPrivateKey")
+
+	issuer := builtin_issuer.NewDataplaneTokenIssuer(func() ([]byte, error) {
+		return privateKey, nil
+	})
+	var authenticator auth.Authenticator
+	var resStore core_store.ResourceStore
+
+	dpRes := core_mesh.DataplaneResource{
+		Meta: &test_model.ResourceMeta{
+			Mesh: "dp-1",
+			Name: "default",
+		},
+		Spec: mesh_proto.Dataplane{
+			Networking: &mesh_proto.Dataplane_Networking{
+				Address: "127.0.0.1",
+				Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
+					{
+						Port:        8080,
+						ServicePort: 8081,
+						Tags: map[string]string{
+							"kuma.io/service":  "web",
+							"kuma.io/protocol": "http",
+						},
+					},
+					{
+						Port:        8090,
+						ServicePort: 8091,
+						Tags: map[string]string{
+							"kuma.io/service":  "web-api",
+							"kuma.io/protocol": "http",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	BeforeEach(func() {
+		resStore = memory.NewStore()
+		authenticator = universal.NewAuthenticator(issuer)
+
+		err := resStore.Create(context.Background(), &dpRes, core_store.CreateByKey("dp-1", "default"))
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	DescribeTable("should correctly authenticate dataplane",
+		func(id builtin_issuer.DataplaneIdentity) {
+			// when
+			credential, err := issuer.Generate(id)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			// when
+			err = authenticator.Authenticate(context.Background(), &dpRes, credential)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+		},
+		Entry("should auth with token bound to nothing", builtin_issuer.DataplaneIdentity{
+			Name: "",
+			Mesh: "",
+			Tags: nil,
+		}),
+		Entry("should auth with token bound to mesh", builtin_issuer.DataplaneIdentity{
+			Mesh: "default",
+		}),
+		Entry("should auth with token bound to mesh and name", builtin_issuer.DataplaneIdentity{
+			Name: "dp-1",
+			Mesh: "default",
+		}),
+		Entry("should auth with token bound to mesh and tags", builtin_issuer.DataplaneIdentity{
+			Mesh: "default",
+			Tags: map[string]map[string]bool{
+				"kuma.io/service": {
+					"web":     true,
+					"web-api": true,
+				},
+			},
+		}),
+	)
+
+	type testCase struct {
+		id  builtin_issuer.DataplaneIdentity
+		err string
+	}
+	DescribeTable("should fail auth",
+		func(given testCase) {
+			// when
+			token, err := issuer.Generate(given.id)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			// when
+			err = authenticator.Authenticate(context.Background(), &dpRes, token)
+
+			// then
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring(given.err))
+		},
+		Entry("on token with different name", testCase{
+			id: builtin_issuer.DataplaneIdentity{
+				Mesh: "default",
+				Name: "dp-2",
+			},
+			err: "proxy name from requestor: dp-1 is different than in token: dp-2",
+		}),
+		Entry("on token with different mesh", testCase{
+			id: builtin_issuer.DataplaneIdentity{
+				Mesh: "demo",
+				Name: "dp-1",
+			},
+			err: "proxy mesh from requestor: default is different than in token: demo",
+		}),
+		Entry("on token with different tags", testCase{
+			id: builtin_issuer.DataplaneIdentity{
+				Tags: map[string]map[string]bool{
+					"kuma.io/service": {
+						"backend": true,
+					},
+				},
+			},
+			err: `which is not allowed with this token. Allowed values in token are ["backend"]`,
+		}),
+		Entry("on token with tag that is absent in dataplane", testCase{
+			id: builtin_issuer.DataplaneIdentity{
+				Tags: map[string]map[string]bool{
+					"kuma.io/zone": {
+						"east": true,
+					},
+				},
+			},
+			err: `dataplane has no tag "kuma.io/zone" required by the token`,
+		}),
+		Entry("on token with missing one tag value", testCase{
+			id: builtin_issuer.DataplaneIdentity{
+				Tags: map[string]map[string]bool{
+					"kuma.io/service": {
+						"web": true,
+						//"web-api": true valid token should have also web-api
+					},
+				},
+			},
+			err: `which is not allowed with this token. Allowed values in token are ["web"]`, // web and web-api order is not stable
+		}),
+	)
+
+	It("should throw an error on invalid token", func() {
+		// when
+		err := authenticator.Authenticate(context.Background(), &dpRes, "this-is-not-valid-jwt-token")
+
+		// then
+		Expect(err).To(MatchError("could not parse token: token contains an invalid number of segments"))
+	})
+
+	It("should throw an error when signing key is not found", func() {
+		// given
+		issuer := builtin_issuer.NewDataplaneTokenIssuer(func() ([]byte, error) {
+			return nil, nil
+		})
+
+		// when
+		_, err := issuer.Generate(builtin_issuer.DataplaneIdentity{})
+
+		// then
+		Expect(err).To(MatchError("there is no Signing Key in the Control Plane. If you run multi-zone setup, make sure Remote is connected to the Global before generating tokens."))
+	})
+})
diff --git a/pkg/xds/auth/universal/noop_authenticator_test.go b/pkg/xds/auth/universal/noop_authenticator_test.go
new file mode 100644
index 000000000..c998fd5e3
--- /dev/null
+++ b/pkg/xds/auth/universal/noop_authenticator_test.go
@@ -0,0 +1,48 @@
+package universal_test
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
+	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
+	"github.com/kumahq/kuma/pkg/xds/auth"
+	"github.com/kumahq/kuma/pkg/xds/auth/universal"
+)
+
+var _ = Describe("Noop Authenticator", func() {
+
+	var authenticator auth.Authenticator
+
+	BeforeEach(func() {
+		authenticator = universal.NewNoopAuthenticator()
+	})
+
+	It("should allow with any token for any dataplane", func() {
+		// given
+		dpRes := core_mesh.DataplaneResource{
+			Spec: mesh_proto.Dataplane{
+				Networking: &mesh_proto.Dataplane_Networking{
+					Address: "127.0.0.1",
+					Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
+						{
+							Port:        8080,
+							ServicePort: 8081,
+							Tags: map[string]string{
+								"kuma.io/service": "web",
+							},
+						},
+					},
+				},
+			},
+		}
+
+		// when
+		err := authenticator.Authenticate(context.Background(), &dpRes, "some-random-token")
+
+		// then
+		Expect(err).ToNot(HaveOccurred())
+	})
+})
diff --git a/pkg/xds/auth/universal/universal_suite_test.go b/pkg/xds/auth/universal/universal_suite_test.go
new file mode 100644
index 000000000..61cb86751
--- /dev/null
+++ b/pkg/xds/auth/universal/universal_suite_test.go
@@ -0,0 +1,13 @@
+package universal_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestUniversal(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Sds Auth Universal Suite")
+}
diff --git a/pkg/xds/bootstrap/generator_test.go b/pkg/xds/bootstrap/generator_test.go
index 316526952..f86b9a479 100644
--- a/pkg/xds/bootstrap/generator_test.go
+++ b/pkg/xds/bootstrap/generator_test.go
@@ -75,7 +75,7 @@ var _ = Describe("bootstrapGenerator", func() {
 	DescribeTable("should generate bootstrap configuration",
 		func(given testCase) {
 			// setup
-			generator := NewDefaultBootstrapGenerator(resManager, given.config(), "")
+			generator := NewDefaultBootstrapGenerator(resManager, given.config(), filepath.Join("..", "..", "..", "test", "certs", "server-cert.pem"))
 
 			// when
 			bootstrapConfig, err := generator.Generate(context.Background(), given.request)
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
index 03c67cea2..65ca34a67 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.k8s.golden.yaml
@@ -2,8 +2,9 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-      - envoyGrpc:
-          clusterName: ads_cluster
+      - googleGrpc:
+          statPrefix: ads
+          targetUri: 127.0.0.1:5678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -11,33 +12,8 @@ dynamicResources:
 node:
   cluster: backend
   id: default.dp-1.default
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-    - connectTimeout: 1s
-      http2ProtocolOptions: {}
-      loadAssignment:
-        clusterName: ads_cluster
-        endpoints:
-          - lbEndpoints:
-              - endpoint:
-                  address:
-                    socketAddress:
-                      address: 127.0.0.1
-                      portValue: 5678
-      name: ads_cluster
-      type: STRICT_DNS
-      upstreamConnectionOptions:
-        tcpKeepalive: {}
     - connectTimeout: 1s
       http2ProtocolOptions: {}
       loadAssignment:
@@ -52,3 +28,13 @@ staticResources:
       type: STATIC
       upstreamConnectionOptions:
         tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml
index 2a6af9edd..3cc695316 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.overridden.golden.yaml
@@ -8,8 +8,17 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-      - envoyGrpc:
-          clusterName: ads_cluster
+      - googleGrpc:
+          callCredentials:
+            - fromPlugin:
+                name: envoy.grpc_credentials.file_based_metadata
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.config.grpc_credential.v2alpha.FileBasedMetadataConfig
+                  secretData:
+                    filename: /tmp/token
+          credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+          statPrefix: ads
+          targetUri: 127.0.0.1:5678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -20,33 +29,8 @@ node:
   metadata:
     dataplane.admin.port: "1234"
     dataplaneTokenPath: /tmp/token
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-    - connectTimeout: 1s
-      http2ProtocolOptions: {}
-      loadAssignment:
-        clusterName: ads_cluster
-        endpoints:
-          - lbEndpoints:
-              - endpoint:
-                  address:
-                    socketAddress:
-                      address: 127.0.0.1
-                      portValue: 5678
-      name: ads_cluster
-      type: STRICT_DNS
-      upstreamConnectionOptions:
-        tcpKeepalive: {}
     - connectTimeout: 1s
       http2ProtocolOptions: {}
       loadAssignment:
@@ -61,3 +45,13 @@ staticResources:
       type: STATIC
       upstreamConnectionOptions:
         tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml b/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
index a0d4f2a6d..93414db99 100644
--- a/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/bootstrap.universal.golden.yaml
@@ -2,8 +2,9 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-      - envoyGrpc:
-          clusterName: ads_cluster
+      - googleGrpc:
+          statPrefix: ads
+          targetUri: 127.0.0.1:5678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -11,33 +12,8 @@ dynamicResources:
 node:
   cluster: backend
   id: default.dp-1
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-    - connectTimeout: 1s
-      http2ProtocolOptions: {}
-      loadAssignment:
-        clusterName: ads_cluster
-        endpoints:
-          - lbEndpoints:
-              - endpoint:
-                  address:
-                    socketAddress:
-                      address: 127.0.0.1
-                      portValue: 5678
-      name: ads_cluster
-      type: STRICT_DNS
-      upstreamConnectionOptions:
-        tcpKeepalive: {}
     - connectTimeout: 1s
       http2ProtocolOptions: {}
       loadAssignment:
@@ -52,3 +28,13 @@ staticResources:
       type: STATIC
       upstreamConnectionOptions:
         tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml b/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml
index 173b2cd9f..7e24bfce9 100644
--- a/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.custom-config-minimal-request.golden.yaml
@@ -8,8 +8,13 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-    - envoyGrpc:
-        clusterName: ads_cluster
+      - googleGrpc:
+          channelCredentials:
+            sslCredentials:
+              rootCerts:
+                inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMRENDQWhTZ0F3SUJBZ0lRSGRQaHhPZlhnV3VOeG9GbFYvRXdxVEFOQmdrcWhraUc5dzBCQVFzRkFEQVAKTVEwd0N3WURWUVFERXdScmRXMWhNQjRYRFRJd01Ea3hOakV5TWpnME5Gb1hEVE13TURreE5ERXlNamcwTkZvdwpEekVOTUFzR0ExVUVBeE1FYTNWdFlUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCCkFPWkdiV2hTbFFTUnhGTnQ1cC8yV0NLRnlIWjNDdXdOZ3lMRVA3blM0Wlh5a3hzRmJZU3VWM2JJZ0Y3YlQvdXEKYTVRaXJlK0M2MGd1aEZicExjUGgyWjZVZmdJZDY5R2xRekhNVlljbUxHalZRdXlBdDRGTU1rVGZWRWw1STRPYQorMml0M0J2aWhWa0toVXo4eTVSUjVLYnFKZkdwNFoyMEZoNmZ0dG9DRmJlT0RtdkJzWUpGbVVRUytpZm95TVkvClAzUjAzU3U3ZzVpSXZuejd0bWt5ZG9OQzhuR1JEemRENUM4Zkp2clZJMVVYNkpSR3lMS3Q0NW9RWHQxbXhLMTAKNUthTjJ6TlYyV3RIc2FKcDlid3JQSCtKaVpHZVp5dnVoNVV3ckxkSENtcUs3c205VG9kR3p0VVpZMFZ6QWM0cQprWVZpWFk4Z1VqZk5tK2NRclBPMWtOOENBd0VBQWFPQmd6Q0JnREFPQmdOVkhROEJBZjhFQkFNQ0FxUXdIUVlEClZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQndNQk1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0hRWUQKVlIwT0JCWUVGR01EQlBQaUJGSjNtdjJvQTlDVHFqZW1GVFYyTUI4R0ExVWRFUVFZTUJhQ0NXeHZZMkZzYUc5egpkSUlKYkc5allXeG9iM04wTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDLzE3UXdlT3BHZGIxTUVCSjhYUEc3CjNzSy91dG9XTFgxdGpmOFN1MURnYTZDRFQvZVRXSFpyV1JmODFLT1ZZMDdkbGU1U1JJREsxUWhmYkdHdEZQK1QKdlprcm9vdXNJOVVTMmFDV2xrZUNaV0dUbnF2TG1Eb091anFhZ0RvS1JSdWs0bVFkdE5Ob254aUwvd1p0VEZLaQorMWlOalVWYkxXaURYZEJMeG9SSVZkTE96cWIvTU54d0VsVXlhVERBa29wUXlPV2FURGtZUHJHbWFXamNzZlBHCmFPS293MHplK3pIVkZxVEhiam5DcUVWM2huc1V5UlV3c0JsbjkrakRKWGd3Wk0vdE1sVkpyWkNoMFNsZTlZNVoKTU9CMGZDZjZzVE1OUlRHZzVMcGw2dUlZTS81SU5wbUhWTW8zbjdNQlNucEVEQVVTMmJmL3VvNWdJaXE2WENkcAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+          statPrefix: ads
+          targetUri: kuma-control-plane.internal:15678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -19,44 +24,29 @@ node:
   id: mesh.name.namespace
   metadata:
     dataplane.admin.port: "9902"
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-  - connectTimeout: 2s
-    http2ProtocolOptions: {}
-    loadAssignment:
-      clusterName: ads_cluster
-      endpoints:
-      - lbEndpoints:
-        - endpoint:
-            address:
-              socketAddress:
-                address: kuma-control-plane.internal
-                portValue: 15678
-    name: ads_cluster
-    type: STRICT_DNS
-    upstreamConnectionOptions:
-      tcpKeepalive: {}
-  - connectTimeout: 2s
-    http2ProtocolOptions: {}
-    loadAssignment:
-      clusterName: access_log_sink
-      endpoints:
-      - lbEndpoints:
-        - endpoint:
-            address:
-              pipe:
-                path: /tmp/kuma-access-logs-name.namespace-mesh.sock
-    name: access_log_sink
-    type: STATIC
-    upstreamConnectionOptions:
-      tcpKeepalive: {}
+    - connectTimeout: 2s
+      http2ProtocolOptions: {}
+      loadAssignment:
+        clusterName: access_log_sink
+        endpoints:
+          - lbEndpoints:
+              - endpoint:
+                  address:
+                    pipe:
+                      path: /tmp/kuma-access-logs-name.namespace-mesh.sock
+      name: access_log_sink
+      type: STATIC
+      upstreamConnectionOptions:
+        tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
index 5d2b17467..1d181fe22 100644
--- a/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.custom-config.golden.yaml
@@ -8,8 +8,21 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-    - envoyGrpc:
-        clusterName: ads_cluster
+      - googleGrpc:
+          callCredentials:
+            - fromPlugin:
+                name: envoy.grpc_credentials.file_based_metadata
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.config.grpc_credential.v2alpha.FileBasedMetadataConfig
+                  secretData:
+                    filename: /tmp/token
+          channelCredentials:
+            sslCredentials:
+              rootCerts:
+                inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMRENDQWhTZ0F3SUJBZ0lRSGRQaHhPZlhnV3VOeG9GbFYvRXdxVEFOQmdrcWhraUc5dzBCQVFzRkFEQVAKTVEwd0N3WURWUVFERXdScmRXMWhNQjRYRFRJd01Ea3hOakV5TWpnME5Gb1hEVE13TURreE5ERXlNamcwTkZvdwpEekVOTUFzR0ExVUVBeE1FYTNWdFlUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCCkFPWkdiV2hTbFFTUnhGTnQ1cC8yV0NLRnlIWjNDdXdOZ3lMRVA3blM0Wlh5a3hzRmJZU3VWM2JJZ0Y3YlQvdXEKYTVRaXJlK0M2MGd1aEZicExjUGgyWjZVZmdJZDY5R2xRekhNVlljbUxHalZRdXlBdDRGTU1rVGZWRWw1STRPYQorMml0M0J2aWhWa0toVXo4eTVSUjVLYnFKZkdwNFoyMEZoNmZ0dG9DRmJlT0RtdkJzWUpGbVVRUytpZm95TVkvClAzUjAzU3U3ZzVpSXZuejd0bWt5ZG9OQzhuR1JEemRENUM4Zkp2clZJMVVYNkpSR3lMS3Q0NW9RWHQxbXhLMTAKNUthTjJ6TlYyV3RIc2FKcDlid3JQSCtKaVpHZVp5dnVoNVV3ckxkSENtcUs3c205VG9kR3p0VVpZMFZ6QWM0cQprWVZpWFk4Z1VqZk5tK2NRclBPMWtOOENBd0VBQWFPQmd6Q0JnREFPQmdOVkhROEJBZjhFQkFNQ0FxUXdIUVlEClZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQndNQk1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0hRWUQKVlIwT0JCWUVGR01EQlBQaUJGSjNtdjJvQTlDVHFqZW1GVFYyTUI4R0ExVWRFUVFZTUJhQ0NXeHZZMkZzYUc5egpkSUlKYkc5allXeG9iM04wTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDLzE3UXdlT3BHZGIxTUVCSjhYUEc3CjNzSy91dG9XTFgxdGpmOFN1MURnYTZDRFQvZVRXSFpyV1JmODFLT1ZZMDdkbGU1U1JJREsxUWhmYkdHdEZQK1QKdlprcm9vdXNJOVVTMmFDV2xrZUNaV0dUbnF2TG1Eb091anFhZ0RvS1JSdWs0bVFkdE5Ob254aUwvd1p0VEZLaQorMWlOalVWYkxXaURYZEJMeG9SSVZkTE96cWIvTU54d0VsVXlhVERBa29wUXlPV2FURGtZUHJHbWFXamNzZlBHCmFPS293MHplK3pIVkZxVEhiam5DcUVWM2huc1V5UlV3c0JsbjkrakRKWGd3Wk0vdE1sVkpyWkNoMFNsZTlZNVoKTU9CMGZDZjZzVE1OUlRHZzVMcGw2dUlZTS81SU5wbUhWTW8zbjdNQlNucEVEQVVTMmJmL3VvNWdJaXE2WENkcAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+          credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+          statPrefix: ads
+          targetUri: kuma-control-plane.internal:15678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -21,44 +34,29 @@ node:
     dataplane.admin.port: "1234"
     dataplane.resource: ' { "type": "Dataplane", "mesh": "mesh", "name": "name.namespace", "creationTime": "1970-01-01T00:00:00Z", "modificationTime": "1970-01-01T00:00:00Z", "networking": { "address": "127.0.0.1", "inbound": [ { "port": 22022, "servicePort": 8443, "tags": { "kuma.io/protocol": "http2", "kuma.io/service": "backend" } }, ] } }'
     dataplaneTokenPath: /tmp/token
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-  - connectTimeout: 2s
-    http2ProtocolOptions: {}
-    loadAssignment:
-      clusterName: ads_cluster
-      endpoints:
-      - lbEndpoints:
-        - endpoint:
-            address:
-              socketAddress:
-                address: kuma-control-plane.internal
-                portValue: 15678
-    name: ads_cluster
-    type: STRICT_DNS
-    upstreamConnectionOptions:
-      tcpKeepalive: {}
-  - connectTimeout: 2s
-    http2ProtocolOptions: {}
-    loadAssignment:
-      clusterName: access_log_sink
-      endpoints:
-      - lbEndpoints:
-        - endpoint:
-            address:
-              pipe:
-                path: /tmp/kuma-access-logs-name.namespace-mesh.sock
-    name: access_log_sink
-    type: STATIC
-    upstreamConnectionOptions:
-      tcpKeepalive: {}
+    - connectTimeout: 2s
+      http2ProtocolOptions: {}
+      loadAssignment:
+        clusterName: access_log_sink
+        endpoints:
+          - lbEndpoints:
+              - endpoint:
+                  address:
+                    pipe:
+                      path: /tmp/kuma-access-logs-name.namespace-mesh.sock
+      name: access_log_sink
+      type: STATIC
+      upstreamConnectionOptions:
+        tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml
index 7d0dee582..330187f03 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config-minimal-request.golden.yaml
@@ -2,8 +2,13 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-      - envoyGrpc:
-          clusterName: ads_cluster
+      - googleGrpc:
+          channelCredentials:
+            sslCredentials:
+              rootCerts:
+                inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMRENDQWhTZ0F3SUJBZ0lRSGRQaHhPZlhnV3VOeG9GbFYvRXdxVEFOQmdrcWhraUc5dzBCQVFzRkFEQVAKTVEwd0N3WURWUVFERXdScmRXMWhNQjRYRFRJd01Ea3hOakV5TWpnME5Gb1hEVE13TURreE5ERXlNamcwTkZvdwpEekVOTUFzR0ExVUVBeE1FYTNWdFlUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCCkFPWkdiV2hTbFFTUnhGTnQ1cC8yV0NLRnlIWjNDdXdOZ3lMRVA3blM0Wlh5a3hzRmJZU3VWM2JJZ0Y3YlQvdXEKYTVRaXJlK0M2MGd1aEZicExjUGgyWjZVZmdJZDY5R2xRekhNVlljbUxHalZRdXlBdDRGTU1rVGZWRWw1STRPYQorMml0M0J2aWhWa0toVXo4eTVSUjVLYnFKZkdwNFoyMEZoNmZ0dG9DRmJlT0RtdkJzWUpGbVVRUytpZm95TVkvClAzUjAzU3U3ZzVpSXZuejd0bWt5ZG9OQzhuR1JEemRENUM4Zkp2clZJMVVYNkpSR3lMS3Q0NW9RWHQxbXhLMTAKNUthTjJ6TlYyV3RIc2FKcDlid3JQSCtKaVpHZVp5dnVoNVV3ckxkSENtcUs3c205VG9kR3p0VVpZMFZ6QWM0cQprWVZpWFk4Z1VqZk5tK2NRclBPMWtOOENBd0VBQWFPQmd6Q0JnREFPQmdOVkhROEJBZjhFQkFNQ0FxUXdIUVlEClZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQndNQk1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0hRWUQKVlIwT0JCWUVGR01EQlBQaUJGSjNtdjJvQTlDVHFqZW1GVFYyTUI4R0ExVWRFUVFZTUJhQ0NXeHZZMkZzYUc5egpkSUlKYkc5allXeG9iM04wTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDLzE3UXdlT3BHZGIxTUVCSjhYUEc3CjNzSy91dG9XTFgxdGpmOFN1MURnYTZDRFQvZVRXSFpyV1JmODFLT1ZZMDdkbGU1U1JJREsxUWhmYkdHdEZQK1QKdlprcm9vdXNJOVVTMmFDV2xrZUNaV0dUbnF2TG1Eb091anFhZ0RvS1JSdWs0bVFkdE5Ob254aUwvd1p0VEZLaQorMWlOalVWYkxXaURYZEJMeG9SSVZkTE96cWIvTU54d0VsVXlhVERBa29wUXlPV2FURGtZUHJHbWFXamNzZlBHCmFPS293MHplK3pIVkZxVEhiam5DcUVWM2huc1V5UlV3c0JsbjkrakRKWGd3Wk0vdE1sVkpyWkNoMFNsZTlZNVoKTU9CMGZDZjZzVE1OUlRHZzVMcGw2dUlZTS81SU5wbUhWTW8zbjdNQlNucEVEQVVTMmJmL3VvNWdJaXE2WENkcAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+          statPrefix: ads
+          targetUri: 127.0.0.1:5678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -11,33 +16,8 @@ dynamicResources:
 node:
   cluster: backend
   id: mesh.name.namespace
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-    - connectTimeout: 1s
-      http2ProtocolOptions: {}
-      loadAssignment:
-        clusterName: ads_cluster
-        endpoints:
-          - lbEndpoints:
-              - endpoint:
-                  address:
-                    socketAddress:
-                      address: 127.0.0.1
-                      portValue: 5678
-      name: ads_cluster
-      type: STRICT_DNS
-      upstreamConnectionOptions:
-        tcpKeepalive: {}
     - connectTimeout: 1s
       http2ProtocolOptions: {}
       loadAssignment:
@@ -52,3 +32,13 @@ staticResources:
       type: STATIC
       upstreamConnectionOptions:
         tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml b/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
index 480dc33a9..8865b8b7b 100644
--- a/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
+++ b/pkg/xds/bootstrap/testdata/generator.default-config.golden.yaml
@@ -8,8 +8,21 @@ dynamicResources:
   adsConfig:
     apiType: GRPC
     grpcServices:
-      - envoyGrpc:
-          clusterName: ads_cluster
+      - googleGrpc:
+          callCredentials:
+            - fromPlugin:
+                name: envoy.grpc_credentials.file_based_metadata
+                typedConfig:
+                  '@type': type.googleapis.com/envoy.config.grpc_credential.v2alpha.FileBasedMetadataConfig
+                  secretData:
+                    filename: /tmp/token
+          channelCredentials:
+            sslCredentials:
+              rootCerts:
+                inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMRENDQWhTZ0F3SUJBZ0lRSGRQaHhPZlhnV3VOeG9GbFYvRXdxVEFOQmdrcWhraUc5dzBCQVFzRkFEQVAKTVEwd0N3WURWUVFERXdScmRXMWhNQjRYRFRJd01Ea3hOakV5TWpnME5Gb1hEVE13TURreE5ERXlNamcwTkZvdwpEekVOTUFzR0ExVUVBeE1FYTNWdFlUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCCkFPWkdiV2hTbFFTUnhGTnQ1cC8yV0NLRnlIWjNDdXdOZ3lMRVA3blM0Wlh5a3hzRmJZU3VWM2JJZ0Y3YlQvdXEKYTVRaXJlK0M2MGd1aEZicExjUGgyWjZVZmdJZDY5R2xRekhNVlljbUxHalZRdXlBdDRGTU1rVGZWRWw1STRPYQorMml0M0J2aWhWa0toVXo4eTVSUjVLYnFKZkdwNFoyMEZoNmZ0dG9DRmJlT0RtdkJzWUpGbVVRUytpZm95TVkvClAzUjAzU3U3ZzVpSXZuejd0bWt5ZG9OQzhuR1JEemRENUM4Zkp2clZJMVVYNkpSR3lMS3Q0NW9RWHQxbXhLMTAKNUthTjJ6TlYyV3RIc2FKcDlid3JQSCtKaVpHZVp5dnVoNVV3ckxkSENtcUs3c205VG9kR3p0VVpZMFZ6QWM0cQprWVZpWFk4Z1VqZk5tK2NRclBPMWtOOENBd0VBQWFPQmd6Q0JnREFPQmdOVkhROEJBZjhFQkFNQ0FxUXdIUVlEClZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQndNQk1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0hRWUQKVlIwT0JCWUVGR01EQlBQaUJGSjNtdjJvQTlDVHFqZW1GVFYyTUI4R0ExVWRFUVFZTUJhQ0NXeHZZMkZzYUc5egpkSUlKYkc5allXeG9iM04wTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDLzE3UXdlT3BHZGIxTUVCSjhYUEc3CjNzSy91dG9XTFgxdGpmOFN1MURnYTZDRFQvZVRXSFpyV1JmODFLT1ZZMDdkbGU1U1JJREsxUWhmYkdHdEZQK1QKdlprcm9vdXNJOVVTMmFDV2xrZUNaV0dUbnF2TG1Eb091anFhZ0RvS1JSdWs0bVFkdE5Ob254aUwvd1p0VEZLaQorMWlOalVWYkxXaURYZEJMeG9SSVZkTE96cWIvTU54d0VsVXlhVERBa29wUXlPV2FURGtZUHJHbWFXamNzZlBHCmFPS293MHplK3pIVkZxVEhiam5DcUVWM2huc1V5UlV3c0JsbjkrakRKWGd3Wk0vdE1sVkpyWkNoMFNsZTlZNVoKTU9CMGZDZjZzVE1OUlRHZzVMcGw2dUlZTS81SU5wbUhWTW8zbjdNQlNucEVEQVVTMmJmL3VvNWdJaXE2WENkcAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+          credentialsFactoryName: envoy.grpc_credentials.file_based_metadata
+          statPrefix: ads
+          targetUri: 127.0.0.1:5678
   cdsConfig:
     ads: {}
   ldsConfig:
@@ -20,33 +33,8 @@ node:
   metadata:
     dataplane.admin.port: "1234"
     dataplaneTokenPath: /tmp/token
-statsConfig:
-  statsTags:
-    - tagName: name
-      regex: '^grpc\.((.+)\.)'
-    - tagName: status
-      regex: '^grpc.*streams_closed(_([0-9]+))'
-    - tagName: worker
-      regex: '(worker_([0-9]+)\.)'
-    - tagName: listener
-      regex: '((.+?)\.)rbac\.'
 staticResources:
   clusters:
-    - connectTimeout: 1s
-      http2ProtocolOptions: {}
-      loadAssignment:
-        clusterName: ads_cluster
-        endpoints:
-          - lbEndpoints:
-              - endpoint:
-                  address:
-                    socketAddress:
-                      address: 127.0.0.1
-                      portValue: 5678
-      name: ads_cluster
-      type: STRICT_DNS
-      upstreamConnectionOptions:
-        tcpKeepalive: {}
     - connectTimeout: 1s
       http2ProtocolOptions: {}
       loadAssignment:
@@ -61,3 +49,13 @@ staticResources:
       type: STATIC
       upstreamConnectionOptions:
         tcpKeepalive: {}
+statsConfig:
+  statsTags:
+    - regex: ^grpc\.((.+)\.)
+      tagName: name
+    - regex: ^grpc.*streams_closed(_([0-9]+))
+      tagName: status
+    - regex: (worker_([0-9]+)\.)
+      tagName: worker
+    - regex: ((.+?)\.)rbac\.
+      tagName: listener
diff --git a/test/e2e/kuma_deploy_global_remote_test.go b/test/e2e/kuma_deploy_global_remote_test.go
index 310e97fe7..7a6c1430a 100644
--- a/test/e2e/kuma_deploy_global_remote_test.go
+++ b/test/e2e/kuma_deploy_global_remote_test.go
@@ -103,7 +103,8 @@ metadata:
 		Expect(err).ToNot(HaveOccurred())
 	})
 
-	It("Should deploy Remote and Global on 2 clusters", func() {
+	It("should deploy Remote and Global on 2 clusters", func() {
+		// when check if remote is online
 		clustersStatus := api_server.Zones{}
 		Eventually(func() (bool, error) {
 			status, response := http_helper.HttpGet(c1.GetTesting(), global.GetGlobaStatusAPI(), nil)
@@ -120,7 +121,7 @@ metadata:
 			return clustersStatus[0].Active, nil
 		}, time.Minute, DefaultTimeout).Should(BeTrue())
 
-		// then
+		// then remote is online
 		found := false
 		for _, cluster := range clustersStatus {
 			if cluster.Address == remote.GetIngressAddress() {
@@ -130,52 +131,12 @@ metadata:
 			}
 		}
 		Expect(found).To(BeTrue())
-	})
 
-	It("should deploy Remote and Global on 2 clusters and sync dataplanes", func() {
-		// given
-		namespace := func(namespace string) string {
-			return fmt.Sprintf(`
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: %s
-`, namespace)
-		}
-		dp := func(cluster, namespace, name string) string {
-			return fmt.Sprintf(`
-apiVersion: kuma.io/v1alpha1
-kind: Dataplane
-mesh: default
-metadata:
-  name: %s
-  namespace: %s
-spec:
-  networking:
-    address: 192.168.0.1
-    inbound:
-      - port: 12343
-        tags:
-          kuma.io/service: backend
-          kuma.io/zone: %s
-    outbound:
-      - port: 1212
-        tags:
-          kuma.io/service: web
-`, name, namespace, cluster)
-		}
-
-		// when
-		err := YamlK8s(namespace("custom-ns"))(c2)
-		Expect(err).ToNot(HaveOccurred())
-		err = YamlK8s(dp("kuma-2-remote", "custom-ns", "dp-1"))(c2)
-		Expect(err).ToNot(HaveOccurred())
-
-		// then
+		// and dataplanes are synced to global
 		Eventually(func() string {
 			output, err := k8s.RunKubectlAndGetOutputE(c1.GetTesting(), c1.GetKubectlOptions("default"), "get", "dataplanes")
 			Expect(err).ToNot(HaveOccurred())
 			return output
-		}, "5s", "500ms").Should(ContainSubstring("kuma-2-remote.dp-1.custom-ns"))
+		}, "5s", "500ms").Should(ContainSubstring("kuma-2-remote.demo-client"))
 	})
 })
diff --git a/test/e2e/kuma_deploy_hybrid_kube_global_test.go b/test/e2e/kuma_deploy_hybrid_kube_global_test.go
index 9e33ce937..50265ee1a 100644
--- a/test/e2e/kuma_deploy_hybrid_kube_global_test.go
+++ b/test/e2e/kuma_deploy_hybrid_kube_global_test.go
@@ -43,6 +43,8 @@ var _ = Describe("Test Kubernetes/Universal deployment when Global is on K8S", f
 		Expect(err).ToNot(HaveOccurred())
 		demoClientToken, err := globalCP.GenerateDpToken("demo-client")
 		Expect(err).ToNot(HaveOccurred())
+		ingressToken, err := globalCP.GenerateDpToken("ingress")
+		Expect(err).ToNot(HaveOccurred())
 
 		// Remote
 		remoteCluster = universalClusters.GetCluster(Kuma3)
@@ -50,6 +52,7 @@ var _ = Describe("Test Kubernetes/Universal deployment when Global is on K8S", f
 			Install(Kuma(core.Remote, WithGlobalAddress(globalCP.GetKDSServerAddress()))).
 			Install(EchoServerUniversal(echoServerToken)).
 			Install(DemoClientUniversal(demoClientToken)).
+			Install(IngressUniversal(ingressToken)).
 			Setup(remoteCluster)
 		Expect(err).ToNot(HaveOccurred())
 		err = remoteCluster.VerifyKuma()
@@ -77,7 +80,7 @@ var _ = Describe("Test Kubernetes/Universal deployment when Global is on K8S", f
 
 	It("communication in between apps in remote zone works", func() {
 		stdout, _, err := remoteCluster.ExecWithRetries("", "", "demo-client",
-			"curl", "-v", "-m", "3", "localhost:4001")
+			"curl", "-v", "-m", "3", "--fail", "localhost:4001")
 		Expect(err).ToNot(HaveOccurred())
 		Expect(stdout).To(ContainSubstring("HTTP/1.1 200 OK"))
 
diff --git a/test/e2e/kuma_deploy_hybrid_test.go b/test/e2e/kuma_deploy_hybrid_test.go
index 04f9d05e3..b3e4c9a41 100644
--- a/test/e2e/kuma_deploy_hybrid_test.go
+++ b/test/e2e/kuma_deploy_hybrid_test.go
@@ -89,6 +89,8 @@ metadata:
 		Expect(err).ToNot(HaveOccurred())
 		demoClientToken, err := globalCP.GenerateDpToken("demo-client")
 		Expect(err).ToNot(HaveOccurred())
+		ingressToken, err := globalCP.GenerateDpToken("ingress")
+		Expect(err).ToNot(HaveOccurred())
 
 		// K8s Cluster 1
 		remote_1 = k8sClusters.GetCluster(Kuma1)
@@ -124,6 +126,7 @@ metadata:
 			Install(Kuma(core.Remote, WithGlobalAddress(globalCP.GetKDSServerAddress()))).
 			Install(EchoServerUniversal(echoServerToken)).
 			Install(DemoClientUniversal(demoClientToken)).
+			Install(IngressUniversal(ingressToken)).
 			Setup(remote_3)
 		Expect(err).ToNot(HaveOccurred())
 		err = remote_3.VerifyKuma()
@@ -135,6 +138,7 @@ metadata:
 		err = NewClusterSetup().
 			Install(Kuma(core.Remote, WithGlobalAddress(globalCP.GetKDSServerAddress()))).
 			Install(DemoClientUniversal(demoClientToken)).
+			Install(IngressUniversal(ingressToken)).
 			Setup(remote_4)
 		Expect(err).ToNot(HaveOccurred())
 		err = remote_4.VerifyKuma()
@@ -215,7 +219,7 @@ metadata:
 
 		// k8s access remote k8s service
 		_, stderr, err := remote_1.ExecWithRetries(TestNamespace, clientPod.GetName(), "demo-client",
-			"curl", "-v", "-m", "3", "echo-server_kuma-test_svc_80.mesh")
+			"curl", "-v", "-m", "3", "--fail", "echo-server_kuma-test_svc_80.mesh")
 		Expect(err).ToNot(HaveOccurred())
 		Expect(stderr).To(ContainSubstring("HTTP/1.1 200 OK"))
 
@@ -234,21 +238,21 @@ metadata:
 
 		// k8s access remote universal service
 		_, stderr, err = remote_2.ExecWithRetries(TestNamespace, clientPod.GetName(), "demo-client",
-			"curl", "-v", "-m", "3", "echo-server_kuma-test_svc_8080.mesh")
+			"curl", "-v", "-m", "3", "--fail", "echo-server_kuma-test_svc_8080.mesh")
 		Expect(err).ToNot(HaveOccurred())
 		Expect(stderr).To(ContainSubstring("HTTP/1.1 200 OK"))
 
 		// Remote 3
 		// universal access remote k8s service
 		stdout, _, err := remote_3.ExecWithRetries("", "", "demo-client",
-			"curl", "-v", "-m", "3", "localhost:4000")
+			"curl", "-v", "-m", "3", "--fail", "localhost:4000")
 		Expect(err).ToNot(HaveOccurred())
 		Expect(stdout).To(ContainSubstring("HTTP/1.1 200 OK"))
 
 		// Remote 4
 		// universal access remote universal service
 		stdout, _, err = remote_4.ExecWithRetries("", "", "demo-client",
-			"curl", "-v", "-m", "3", "localhost:4001")
+			"curl", "-v", "-m", "3", "--fail", "localhost:4001")
 		Expect(err).ToNot(HaveOccurred())
 		Expect(stdout).To(ContainSubstring("HTTP/1.1 200 OK"))
 	})
@@ -258,7 +262,7 @@ metadata:
 		// universal access remote universal service
 		Eventually(func() (string, error) {
 			stdout, _, err := remote_4.ExecWithRetries("", "", "demo-client",
-				"curl", "-v", "-m", "3", "localhost:4001")
+				"curl", "-v", "-m", "3", "--fail", "localhost:4001")
 			return stdout, err
 		}, "10s", "1s").Should(ContainSubstring("HTTP/1.1 200 OK"))
 
@@ -272,7 +276,7 @@ metadata:
 		// universal access remote k8s service
 		Eventually(func() (string, error) {
 			stdout, _, err := remote_3.ExecWithRetries("", "", "demo-client",
-				"curl", "-v", "-m", "3", "localhost:4000")
+				"curl", "-v", "-m", "3", "--fail", "localhost:4000")
 			return stdout, err
 		}, "10s", "1s").Should(ContainSubstring("HTTP/1.1 200 OK"))
 
diff --git a/test/e2e/kuma_deploy_universal_test.go b/test/e2e/kuma_deploy_universal_test.go
index fbe3b4144..18ad7e1bf 100644
--- a/test/e2e/kuma_deploy_universal_test.go
+++ b/test/e2e/kuma_deploy_universal_test.go
@@ -60,6 +60,8 @@ destinations:
 		Expect(err).ToNot(HaveOccurred())
 		demoClientToken, err := globalCP.GenerateDpToken("demo-client")
 		Expect(err).ToNot(HaveOccurred())
+		ingressToken, err := globalCP.GenerateDpToken("ingress")
+		Expect(err).ToNot(HaveOccurred())
 
 		// Cluster 1
 		remote_1 = clusters.GetCluster(Kuma2)
@@ -68,6 +70,7 @@ destinations:
 			Install(Kuma(core.Remote, WithGlobalAddress(globalCP.GetKDSServerAddress()))).
 			Install(EchoServerUniversal(echoServerToken)).
 			Install(DemoClientUniversal(demoClientToken)).
+			Install(IngressUniversal(ingressToken)).
 			Setup(remote_1)
 		Expect(err).ToNot(HaveOccurred())
 		err = remote_1.VerifyKuma()
@@ -79,6 +82,7 @@ destinations:
 		err = NewClusterSetup().
 			Install(Kuma(core.Remote, WithGlobalAddress(globalCP.GetKDSServerAddress()))).
 			Install(DemoClientUniversal(demoClientToken)).
+			Install(IngressUniversal(ingressToken)).
 			Setup(remote_2)
 		Expect(err).ToNot(HaveOccurred())
 		err = remote_2.VerifyKuma()
@@ -119,15 +123,24 @@ destinations:
 	})
 
 	It("Should deploy two apps", func() {
-		stdout, _, err := remote_1.ExecWithRetries("", "", "demo-client",
-			"curl", "-v", "-m", "3", "localhost:4001")
-		Expect(err).ToNot(HaveOccurred())
-		Expect(stdout).To(ContainSubstring("HTTP/1.1 200 OK"))
+		retry.DoWithRetry(remote_1.GetTesting(), "curl local service",
+			DefaultRetries, DefaultTimeout,
+			func() (string, error) {
+				stdout, _, err := remote_1.ExecWithRetries("", "", "demo-client",
+					"curl", "-v", "-m", "3", "localhost:4001")
+				if err != nil {
+					return "should retry", err
+				}
+				if strings.Contains(stdout, "HTTP/1.1 200 OK") {
+					return "Accessing service successful", nil
+				}
+				return "should retry", errors.Errorf("should retry")
+			})
 
 		retry.DoWithRetry(remote_2.GetTesting(), "curl remote service",
 			DefaultRetries, DefaultTimeout,
 			func() (string, error) {
-				stdout, _, err = remote_2.ExecWithRetries("", "", "demo-client",
+				stdout, _, err := remote_2.ExecWithRetries("", "", "demo-client",
 					"curl", "-v", "-m", "3", "localhost:4001")
 				if err != nil {
 					return "should retry", err
diff --git a/test/e2e/kuma_helm_deploy_global_remote_test.go b/test/e2e/kuma_helm_deploy_global_remote_test.go
index 09326cdd6..030028814 100644
--- a/test/e2e/kuma_helm_deploy_global_remote_test.go
+++ b/test/e2e/kuma_helm_deploy_global_remote_test.go
@@ -145,52 +145,12 @@ metadata:
 			}
 		}
 		Expect(found).To(BeTrue())
-	})
-
-	It("should deploy Remote and Global on 2 clusters and sync dataplanes", func() {
-		// given
-		namespace := func(namespace string) string {
-			return fmt.Sprintf(`
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: %s
-`, namespace)
-		}
-		dp := func(cluster, namespace, name string) string {
-			return fmt.Sprintf(`
-apiVersion: kuma.io/v1alpha1
-kind: Dataplane
-mesh: default
-metadata:
-  name: %s
-  namespace: %s
-spec:
-  networking:
-    address: 192.168.0.1
-    inbound:
-      - port: 12343
-        tags:
-          kuma.io/service: backend
-          kuma.io/zone: %s
-    outbound:
-      - port: 1212
-        tags:
-          kuma.io/service: web
-`, name, namespace, cluster)
-		}
-
-		// when
-		err := YamlK8s(namespace("custom-ns"))(c2)
-		Expect(err).ToNot(HaveOccurred())
-		err = YamlK8s(dp("kuma-2-remote", "custom-ns", "dp-1"))(c2)
-		Expect(err).ToNot(HaveOccurred())
 
-		// then
+		// and dataplanes are synced to global
 		Eventually(func() string {
 			output, err := k8s.RunKubectlAndGetOutputE(c1.GetTesting(), c1.GetKubectlOptions("default"), "get", "dataplanes")
 			Expect(err).ToNot(HaveOccurred())
 			return output
-		}, "5s", "500ms").Should(ContainSubstring("kuma-2-remote.dp-1.custom-ns"))
+		}, "5s", "500ms").Should(ContainSubstring("kuma-2-remote.demo-client"))
 	})
 })
diff --git a/test/e2e/tracing_kubernetes_test.go b/test/e2e/tracing_kubernetes_test.go
index 6783b437d..69a765fe7 100644
--- a/test/e2e/tracing_kubernetes_test.go
+++ b/test/e2e/tracing_kubernetes_test.go
@@ -107,7 +107,7 @@ spec:
 			DefaultRetries, DefaultTimeout,
 			func() (string, error) {
 				_, _, err := cluster.ExecWithRetries(TestNamespace, clientPod.GetName(), "demo-client",
-					"curl", "-v", "-m", "3", "echo-server")
+					"curl", "-v", "-m", "3", "--fail", "echo-server")
 				if err != nil {
 					return "", err
 				}
diff --git a/test/e2e/tracing_universal_test.go b/test/e2e/tracing_universal_test.go
index c66717ffd..1de880cd5 100644
--- a/test/e2e/tracing_universal_test.go
+++ b/test/e2e/tracing_universal_test.go
@@ -77,7 +77,7 @@ selectors:
 
 		retry.DoWithRetry(cluster.GetTesting(), "check traced services", DefaultRetries, DefaultTimeout, func() (string, error) {
 			// when client sends requests to server
-			_, _, err := cluster.Exec("", "", "demo-client", "curl", "-v", "-m", "3", "localhost:4001")
+			_, _, err := cluster.Exec("", "", "demo-client", "curl", "-v", "-m", "3", "--fail", "localhost:4001")
 			if err != nil {
 				return "", err
 			}
diff --git a/test/framework/k8s_cluster.go b/test/framework/k8s_cluster.go
index fd73dbc5a..20e77b7e6 100644
--- a/test/framework/k8s_cluster.go
+++ b/test/framework/k8s_cluster.go
@@ -485,6 +485,15 @@ func (c *K8sCluster) deleteKumaViaHelm(opts *deployOptions) (errs error) {
 		errs = multierr.Append(errs, err)
 	}
 
+	// HELM does not remove CRDs therefore we need to do it manually.
+	// It's important to remove CRDs to get rid of all "instances" of CRDs like default Mesh etc.
+	crdsYAML, err := k8s.RunKubectlAndGetOutputE(c.t, c.GetKubectlOptions(), "get", "crds", "-o", "yaml")
+	if err != nil {
+		errs = multierr.Append(errs, err)
+	} else if err := k8s.KubectlDeleteFromStringE(c.t, c.GetKubectlOptions(), crdsYAML); err != nil {
+		errs = multierr.Append(errs, err)
+	}
+
 	return errs
 }
 
diff --git a/test/framework/setup.go b/test/framework/setup.go
index 26ecb01fe..26e8637cb 100644
--- a/test/framework/setup.go
+++ b/test/framework/setup.go
@@ -112,6 +112,24 @@ type IngressDesc struct {
 	IP   string
 }
 
+func IngressUniversal(token string) InstallFunc {
+	return func(cluster Cluster) error {
+		uniCluster := cluster.(*UniversalCluster)
+		app, err := NewUniversalApp(cluster.GetTesting(), uniCluster.name, AppIngress, true, []string{}, []string{})
+		if err != nil {
+			return err
+		}
+		err = app.mainApp.Start()
+		if err != nil {
+			return err
+		}
+		uniCluster.apps[AppIngress] = app
+
+		dpyaml := fmt.Sprintf(IngressDataplane, kdsPort)
+		return uniCluster.CreateDP(app, "ingress", app.ip, dpyaml, token)
+	}
+}
+
 func DemoClientK8s() InstallFunc {
 	const name = "demo-client"
 	return Combine(
diff --git a/test/framework/universal_app.go b/test/framework/universal_app.go
index 73e7d71eb..c15b8bd77 100644
--- a/test/framework/universal_app.go
+++ b/test/framework/universal_app.go
@@ -22,6 +22,7 @@ type AppMode string
 
 const (
 	AppModeCP         = "kuma-cp"
+	AppIngress        = "ingress"
 	AppModeEchoServer = "echo-server"
 	sshPort           = "22"
 
diff --git a/test/framework/universal_cluster.go b/test/framework/universal_cluster.go
index 874327e0a..d67488932 100644
--- a/test/framework/universal_cluster.go
+++ b/test/framework/universal_cluster.go
@@ -90,15 +90,6 @@ func (c *UniversalCluster) DeployKuma(mode string, fs ...DeployOptionsFunc) erro
 
 	c.apps[AppModeCP] = app
 
-	switch mode {
-	case core.Remote:
-		dpyaml := fmt.Sprintf(IngressDataplane, kdsPort)
-		err = c.CreateDP(app, "ingress", app.ip, dpyaml, "XYZ") // todo token is static now, Ingress does not access SDS so the token does not matter for now
-		if err != nil {
-			return err
-		}
-	}
-
 	return nil
 }
 
diff --git a/test/framework/universal_controlplane.go b/test/framework/universal_controlplane.go
index 6ec2d8930..34c06ce02 100644
--- a/test/framework/universal_controlplane.go
+++ b/test/framework/universal_controlplane.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strconv"
 
+	"github.com/gruntwork-io/terratest/modules/retry"
 	"github.com/pkg/errors"
 
 	"github.com/kumahq/kuma/pkg/config/core"
@@ -49,7 +50,7 @@ func (c *UniversalControlPlane) GetKDSServerAddress() string {
 }
 
 func (c *UniversalControlPlane) GetIngressAddress() string {
-	return c.cluster.apps[AppModeCP].ip + ":" + strconv.FormatUint(uint64(kdsPort), 10)
+	return c.cluster.apps[AppIngress].ip + ":" + strconv.FormatUint(uint64(kdsPort), 10)
 }
 
 func (c *UniversalControlPlane) GetGlobaStatusAPI() string {
@@ -57,16 +58,18 @@ func (c *UniversalControlPlane) GetGlobaStatusAPI() string {
 }
 
 func (c *UniversalControlPlane) GenerateDpToken(service string) (string, error) {
-	sshApp := NewSshApp(c.verbose, c.cluster.apps[AppModeCP].ports["22"], []string{}, []string{"curl",
-		"--fail", "--show-error",
-		"-H", "\"Content-Type: application/json\"",
-		"--data", fmt.Sprintf(`'{"mesh": "default", "tags": {"kuma.io/service":["%s"]}}'`, service),
-		"http://localhost:5679/tokens"})
-	if err := sshApp.Run(); err != nil {
-		return "", err
-	}
-	if sshApp.Err() != "" {
-		return "", errors.New(sshApp.Err())
-	}
-	return sshApp.Out(), nil
+	return retry.DoWithRetryE(c.t, "generating DP token", DefaultRetries, DefaultTimeout, func() (string, error) {
+		sshApp := NewSshApp(c.verbose, c.cluster.apps[AppModeCP].ports["22"], []string{}, []string{"curl",
+			"--fail", "--show-error",
+			"-H", "\"Content-Type: application/json\"",
+			"--data", fmt.Sprintf(`'{"mesh": "default", "tags": {"kuma.io/service":["%s"]}}'`, service),
+			"http://localhost:5679/tokens"})
+		if err := sshApp.Run(); err != nil {
+			return "", err
+		}
+		if sshApp.Err() != "" {
+			return "", errors.New(sshApp.Err())
+		}
+		return sshApp.Out(), nil
+	})
 }