Repository: kumahq/kuma
Pull request #1004 (2020-09-02 11:57:52)

feat(kuma-cp) dataplane token boundary improvements
This PR improves Dataplane Token so instead of generating it for mandatory mesh+name you can generate it by
1) nothing, so you can use it across all DPs and Meshes
2) Mesh, so you can use it across all DPs and given mesh
3) Mesh+Name - current behavior
4) Mesh+Tag, so you can use it across all instances of one service

### Documentation

- [ ] TODO



Patch:
diff --git a/api/mesh/v1alpha1/dataplane_helpers.go b/api/mesh/v1alpha1/dataplane_helpers.go
index 6b1a1c51b..92fec5f00 100644
--- a/api/mesh/v1alpha1/dataplane_helpers.go
+++ b/api/mesh/v1alpha1/dataplane_helpers.go
@@ -274,6 +274,21 @@ func (t MultiValueTagSet) Values(key string) []string {
 	return result
 }
 
+func MultiValueTagSetFrom(data map[string][]string) MultiValueTagSet {
+	set := MultiValueTagSet{}
+	for tagName, values := range data {
+		for _, value := range values {
+			m, ok := set[tagName]
+			if !ok {
+				m = map[string]bool{}
+			}
+			m[value] = true
+			set[tagName] = m
+		}
+	}
+	return set
+}
+
 func (d *Dataplane) Tags() MultiValueTagSet {
 	tags := MultiValueTagSet{}
 	for _, inbound := range d.GetNetworking().GetInbound() {
diff --git a/app/kumactl/cmd/generate/generate_dataplane_token.go b/app/kumactl/cmd/generate/generate_dataplane_token.go
index edc04fac2..7a4ee8579 100644
--- a/app/kumactl/cmd/generate/generate_dataplane_token.go
+++ b/app/kumactl/cmd/generate/generate_dataplane_token.go
@@ -1,6 +1,8 @@
 package generate
 
 import (
+	"strings"
+
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
@@ -12,6 +14,7 @@ type generateDataplaneTokenContext struct {
 
 	args struct {
 		dataplane string
+		tags      map[string]string
 	}
 }
 
@@ -21,13 +24,27 @@ func NewGenerateDataplaneTokenCmd(pctx *kumactl_cmd.RootContext) *cobra.Command
 		Use:   "dataplane-token",
 		Short: "Generate Dataplane Token",
 		Long:  `Generate Dataplane Token that is used to prove Dataplane identity.`,
+		Example: `
+Generate token bound by name and mesh
+$ kumactl generate dataplane-token --mesh demo --dataplane demo-01
+
+Generate token bound by mesh
+$ kumactl generate dataplane-token --mesh demo
+
+Generate token bound by tag
+$ kumactl generate dataplane-token --mesh demo --tag kuma.io/service=web,web-api
+`,
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			client, err := pctx.CurrentDataplaneTokenClient()
 			if err != nil {
 				return errors.Wrap(err, "failed to create dataplane token client")
 			}
 
-			token, err := client.Generate(ctx.args.dataplane, pctx.Args.Mesh)
+			tags := map[string][]string{}
+			for k, v := range ctx.args.tags {
+				tags[k] = strings.Split(v, ",")
+			}
+			token, err := client.Generate(ctx.args.dataplane, pctx.Args.Mesh, tags)
 			if err != nil {
 				return errors.Wrap(err, "failed to generate a dataplane token")
 			}
@@ -36,6 +53,6 @@ func NewGenerateDataplaneTokenCmd(pctx *kumactl_cmd.RootContext) *cobra.Command
 		},
 	}
 	cmd.Flags().StringVar(&ctx.args.dataplane, "dataplane", "", "name of the Dataplane")
-	_ = cmd.MarkFlagRequired("dataplane")
+	cmd.Flags().StringToStringVar(&ctx.args.tags, "tag", nil, "required tag values for dataplane (split values by comma to provide multiple values)")
 	return cmd
 }
diff --git a/app/kumactl/pkg/tokens/client.go b/app/kumactl/pkg/tokens/client.go
index 05a9b056a..80ba2e08a 100644
--- a/app/kumactl/pkg/tokens/client.go
+++ b/app/kumactl/pkg/tokens/client.go
@@ -43,7 +43,7 @@ func NewDataplaneTokenClient(address string, config *kumactl_config.Context_Admi
 }
 
 type DataplaneTokenClient interface {
-	Generate(name string, mesh string) (string, error)
+	Generate(name string, mesh string, tags map[string][]string) (string, error)
 }
 
 type httpDataplaneTokenClient struct {
@@ -52,10 +52,11 @@ type httpDataplaneTokenClient struct {
 
 var _ DataplaneTokenClient = &httpDataplaneTokenClient{}
 
-func (h *httpDataplaneTokenClient) Generate(name string, mesh string) (string, error) {
+func (h *httpDataplaneTokenClient) Generate(name string, mesh string, tags map[string][]string) (string, error) {
 	tokenReq := &types.DataplaneTokenRequest{
 		Name: name,
 		Mesh: mesh,
+		Tags: tags,
 	}
 	reqBytes, err := json.Marshal(tokenReq)
 	if err != nil {
diff --git a/pkg/sds/auth/universal/authenticator.go b/pkg/sds/auth/universal/authenticator.go
index 73b12b789..c246d552b 100644
--- a/pkg/sds/auth/universal/authenticator.go
+++ b/pkg/sds/auth/universal/authenticator.go
@@ -5,6 +5,8 @@ import (
 
 	"github.com/pkg/errors"
 
+	"github.com/kumahq/kuma/api/mesh/v1alpha1"
+	"github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	sds_auth "github.com/kumahq/kuma/pkg/sds/auth"
 	common_auth "github.com/kumahq/kuma/pkg/sds/auth/common"
@@ -18,34 +20,66 @@ func NewAuthenticator(issuer builtin_issuer.DataplaneTokenIssuer, dataplaneResol
 	}
 }
 
+// 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
 	dataplaneResolver common_auth.DataplaneResolver
 }
 
 func (u *universalAuthenticator) Authenticate(ctx context.Context, proxyId core_xds.ProxyId, credential sds_auth.Credential) (sds_auth.Identity, error) {
-	if err := u.reviewToken(proxyId, credential); err != nil {
-		return sds_auth.Identity{}, err
-	}
-
 	dataplane, err := u.dataplaneResolver(ctx, proxyId)
 	if err != nil {
 		return sds_auth.Identity{}, errors.Wrapf(err, "unable to find Dataplane for proxy %q", proxyId)
 	}
+	if err := u.reviewToken(dataplane, credential); err != nil {
+		return sds_auth.Identity{}, err
+	}
 	return common_auth.GetDataplaneIdentity(dataplane)
 }
 
-func (u *universalAuthenticator) reviewToken(expectedId core_xds.ProxyId, credential sds_auth.Credential) error {
-	proxyId, err := u.issuer.Validate(credential)
+func (u *universalAuthenticator) reviewToken(dataplane *mesh.DataplaneResource, credential sds_auth.Credential) error {
+	dpIdentity, err := u.issuer.Validate(credential)
 	if err != nil {
 		return err
 	}
 
-	if expectedId.Name != proxyId.Name {
-		return errors.Errorf("proxy name from requestor: %s is different than in token: %s", expectedId.Name, proxyId.Name)
+	if dpIdentity.Name != "" {
+		if 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 != "" {
+		if 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 expectedId.Mesh != proxyId.Mesh {
-		return errors.Errorf("proxy mesh from requestor: %s is different than in token: %s", expectedId.Mesh, proxyId.Mesh)
+	if err := validateTags(dpIdentity.Tags, dataplane.Spec.Tags()); err != nil {
+		return err
+	}
+	return nil
+}
+
+func validateTags(tokenTags v1alpha1.MultiValueTagSet, dpTags v1alpha1.MultiValueTagSet) error {
+	if len(tokenTags) != 0 {
+		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/sds/server/auth_callbacks.go b/pkg/sds/server/auth_callbacks.go
index 1f014db18..5e33190d9 100644
--- a/pkg/sds/server/auth_callbacks.go
+++ b/pkg/sds/server/auth_callbacks.go
@@ -70,7 +70,7 @@ func (a *authCallbacks) authenticate(credential sds_auth.Credential, req *envoy_
 
 	_, err = a.authenticator.Authenticate(context.Background(), *proxyId, credential)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "authentication failed")
 	}
 	return nil
 }
diff --git a/pkg/tokens/builtin/issuer/issuer.go b/pkg/tokens/builtin/issuer/issuer.go
index 122083efe..91d347ad3 100644
--- a/pkg/tokens/builtin/issuer/issuer.go
+++ b/pkg/tokens/builtin/issuer/issuer.go
@@ -4,18 +4,28 @@ import (
 	"github.com/dgrijalva/jwt-go"
 	"github.com/pkg/errors"
 
-	"github.com/kumahq/kuma/pkg/core/xds"
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	"github.com/kumahq/kuma/pkg/sds/auth"
 )
 
+type DataplaneIdentity struct {
+	Name string
+	Mesh string
+	Tags mesh_proto.MultiValueTagSet
+}
+
+// DataplaneTokenIssuer issues Dataplane Tokens used then for proving identity of the dataplanes.
+// 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(proxyId xds.ProxyId) (auth.Credential, error)
-	Validate(credential auth.Credential) (xds.ProxyId, error)
+	Generate(identity DataplaneIdentity) (auth.Credential, error)
+	Validate(credential auth.Credential) (DataplaneIdentity, error)
 }
 
 type claims struct {
 	Name string
 	Mesh string
+	Tags map[string][]string
 	jwt.StandardClaims
 }
 
@@ -29,10 +39,16 @@ type jwtTokenIssuer struct {
 	privateKey []byte
 }
 
-func (i *jwtTokenIssuer) Generate(proxyId xds.ProxyId) (auth.Credential, error) {
+func (i *jwtTokenIssuer) Generate(identity DataplaneIdentity) (auth.Credential, error) {
+	tags := map[string][]string{}
+	for tagName := range identity.Tags {
+		tags[tagName] = identity.Tags.Values(tagName)
+	}
+
 	c := claims{
-		Name:           proxyId.Name,
-		Mesh:           proxyId.Mesh,
+		Name:           identity.Name,
+		Mesh:           identity.Mesh,
+		Tags:           tags,
 		StandardClaims: jwt.StandardClaims{},
 	}
 
@@ -44,22 +60,23 @@ func (i *jwtTokenIssuer) Generate(proxyId xds.ProxyId) (auth.Credential, error)
 	return auth.Credential(tokenString), nil
 }
 
-func (i *jwtTokenIssuer) Validate(credential auth.Credential) (xds.ProxyId, error) {
+func (i *jwtTokenIssuer) Validate(credential auth.Credential) (DataplaneIdentity, error) {
 	c := &claims{}
 
 	token, err := jwt.ParseWithClaims(string(credential), c, func(*jwt.Token) (interface{}, error) {
 		return i.privateKey, nil
 	})
 	if err != nil {
-		return xds.ProxyId{}, errors.Wrap(err, "could not parse token")
+		return DataplaneIdentity{}, errors.Wrap(err, "could not parse token")
 	}
 	if !token.Valid {
-		return xds.ProxyId{}, errors.New("token is not valid")
+		return DataplaneIdentity{}, errors.New("token is not valid")
 	}
 
-	id := xds.ProxyId{
+	id := DataplaneIdentity{
 		Mesh: c.Mesh,
 		Name: c.Name,
+		Tags: mesh_proto.MultiValueTagSetFrom(c.Tags),
 	}
 	return id, nil
 }
diff --git a/pkg/tokens/builtin/server/types/dataplane_token_request.go b/pkg/tokens/builtin/server/types/dataplane_token_request.go
index 7c5882622..576178819 100644
--- a/pkg/tokens/builtin/server/types/dataplane_token_request.go
+++ b/pkg/tokens/builtin/server/types/dataplane_token_request.go
@@ -1,15 +1,7 @@
 package types
 
-import "github.com/kumahq/kuma/pkg/core/xds"
-
 type DataplaneTokenRequest struct {
-	Name string `json:"name"`
-	Mesh string `json:"mesh"`
-}
-
-func (i DataplaneTokenRequest) ToProxyId() xds.ProxyId {
-	return xds.ProxyId{
-		Mesh: i.Mesh,
-		Name: i.Name,
-	}
+	Name string              `json:"name"`
+	Mesh string              `json:"mesh"`
+	Tags map[string][]string `json:"tags"`
 }
diff --git a/pkg/tokens/builtin/server/webservice.go b/pkg/tokens/builtin/server/webservice.go
index 4ef296ebf..acf880b76 100644
--- a/pkg/tokens/builtin/server/webservice.go
+++ b/pkg/tokens/builtin/server/webservice.go
@@ -5,9 +5,9 @@ import (
 
 	"github.com/emicklei/go-restful"
 
+	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	"github.com/kumahq/kuma/pkg/core"
 	"github.com/kumahq/kuma/pkg/core/rest/errors"
-	"github.com/kumahq/kuma/pkg/core/validators"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/server/types"
 )
@@ -41,19 +41,12 @@ func (d *dataplaneTokenWebService) handleIdentityRequest(request *restful.Reques
 		response.WriteHeader(http.StatusBadRequest)
 		return
 	}
-	verr := validators.ValidationError{}
-	if idReq.Name == "" {
-		verr.AddViolation("name", "cannot be empty")
-	}
-	if idReq.Mesh == "" {
-		verr.AddViolation("mesh", "cannot be empty")
-	}
-	if verr.HasViolations() {
-		errors.HandleError(response, verr.OrNil(), "Invalid request")
-		return
-	}
 
-	token, err := d.issuer.Generate(idReq.ToProxyId())
+	token, err := d.issuer.Generate(issuer.DataplaneIdentity{
+		Mesh: idReq.Mesh,
+		Name: idReq.Name,
+		Tags: mesh_proto.MultiValueTagSetFrom(idReq.Tags),
+	})
 	if err != nil {
 		errors.HandleError(response, err, "Could not issue a token")
 		return


Test patch:
diff --git a/app/kumactl/cmd/completion/testdata/bash.golden b/app/kumactl/cmd/completion/testdata/bash.golden
index 9eb9b0ef0..ba7d26d5d 100644
--- a/app/kumactl/cmd/completion/testdata/bash.golden
+++ b/app/kumactl/cmd/completion/testdata/bash.golden
@@ -733,6 +733,9 @@ _kumactl_generate_dataplane-token()
     flags+=("--dataplane=")
     two_word_flags+=("--dataplane")
     local_nonpersistent_flags+=("--dataplane=")
+    flags+=("--tag=")
+    two_word_flags+=("--tag")
+    local_nonpersistent_flags+=("--tag=")
     flags+=("--config-file=")
     two_word_flags+=("--config-file")
     flags+=("--log-level=")
@@ -742,7 +745,6 @@ _kumactl_generate_dataplane-token()
     two_word_flags+=("-m")
 
     must_have_one_flag=()
-    must_have_one_flag+=("--dataplane=")
     must_have_one_noun=()
     noun_aliases=()
 }
diff --git a/app/kumactl/cmd/completion/testdata/zsh.golden b/app/kumactl/cmd/completion/testdata/zsh.golden
index f649d487f..b6e8b0ea3 100644
--- a/app/kumactl/cmd/completion/testdata/zsh.golden
+++ b/app/kumactl/cmd/completion/testdata/zsh.golden
@@ -283,6 +283,7 @@ function _kumactl_generate {
 function _kumactl_generate_dataplane-token {
   _arguments \
     '--dataplane[name of the Dataplane]:' \
+    '--tag[required tag values for dataplane (split values by comma to provide multiple values)]:' \
     '--config-file[path to the configuration file to use]:' \
     '--log-level[log level: one of off|info|debug]:' \
     '(-m --mesh)'{-m,--mesh}'[mesh to use]:'
diff --git a/app/kumactl/cmd/generate/generate_dataplane_token_test.go b/app/kumactl/cmd/generate/generate_dataplane_token_test.go
index 8e9ed6c02..62c0fa85d 100644
--- a/app/kumactl/cmd/generate/generate_dataplane_token_test.go
+++ b/app/kumactl/cmd/generate/generate_dataplane_token_test.go
@@ -24,7 +24,7 @@ type staticDataplaneTokenGenerator struct {
 
 var _ tokens.DataplaneTokenClient = &staticDataplaneTokenGenerator{}
 
-func (s *staticDataplaneTokenGenerator) Generate(name string, mesh string) (string, error) {
+func (s *staticDataplaneTokenGenerator) Generate(name string, mesh string, tags map[string][]string) (string, error) {
 	if s.err != nil {
 		return "", s.err
 	}
diff --git a/app/kumactl/pkg/tokens/client_test.go b/app/kumactl/pkg/tokens/client_test.go
index 9cde9b9af..d24ae9e3f 100644
--- a/app/kumactl/pkg/tokens/client_test.go
+++ b/app/kumactl/pkg/tokens/client_test.go
@@ -15,7 +15,6 @@ import (
 	admin_server "github.com/kumahq/kuma/pkg/admin-server"
 	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/core/xds"
 	"github.com/kumahq/kuma/pkg/sds/auth"
 	"github.com/kumahq/kuma/pkg/test"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
@@ -27,12 +26,12 @@ type staticTokenIssuer struct {
 
 var _ issuer.DataplaneTokenIssuer = &staticTokenIssuer{}
 
-func (s *staticTokenIssuer) Generate(proxyId xds.ProxyId) (auth.Credential, error) {
-	return auth.Credential(fmt.Sprintf("token-for-%s-%s", proxyId.Name, proxyId.Mesh)), nil
+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) Validate(credential auth.Credential) (xds.ProxyId, error) {
-	return xds.ProxyId{}, errors.New("not implemented")
+func (s *staticTokenIssuer) Validate(credential auth.Credential) (issuer.DataplaneIdentity, error) {
+	return issuer.DataplaneIdentity{}, errors.New("not implemented")
 }
 
 var _ = Describe("Tokens Client", func() {
@@ -88,12 +87,12 @@ var _ = Describe("Tokens Client", func() {
 
 			// wait for server
 			Eventually(func() error {
-				_, err := client.Generate("example", "default")
+				_, err := client.Generate("example", "default", nil)
 				return err
 			}, "5s", "100ms").ShouldNot(HaveOccurred())
 
 			// when
-			token, err := client.Generate("example", "default")
+			token, err := client.Generate("example", "default", nil)
 
 			// then
 			Expect(err).ToNot(HaveOccurred())
@@ -128,7 +127,7 @@ var _ = Describe("Tokens Client", func() {
 		Expect(err).ToNot(HaveOccurred())
 
 		// when
-		_, err = client.Generate("example", "default")
+		_, err = client.Generate("example", "default", nil)
 
 		// then
 		Expect(err).To(MatchError("unexpected status code 500. Expected 200"))
diff --git a/pkg/sds/auth/universal/auth_test.go b/pkg/sds/auth/universal/auth_test.go
index f1bb8fd29..45ab91cd7 100644
--- a/pkg/sds/auth/universal/auth_test.go
+++ b/pkg/sds/auth/universal/auth_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 
 	"github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -31,14 +32,6 @@ var _ = Describe("Authentication flow", func() {
 			issuer,
 			server.DefaultDataplaneResolver(manager.NewResourceManager(resStore)),
 		)
-	})
-
-	It("should correctly authenticate dataplane", func() {
-		// given
-		id := xds.ProxyId{
-			Mesh: "example",
-			Name: "dp-1",
-		}
 
 		dpRes := core_mesh.DataplaneResource{
 			Spec: v1alpha1.Dataplane{
@@ -49,90 +42,153 @@ var _ = Describe("Authentication flow", func() {
 							Port:        8080,
 							ServicePort: 8081,
 							Tags: map[string]string{
-								"kuma.io/service": "web",
+								"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",
 							},
 						},
 					},
 				},
 			},
 		}
-		err := resStore.Create(context.Background(), &dpRes, store.CreateBy(id.ToResourceKey()))
+		err := resStore.Create(context.Background(), &dpRes, store.CreateByKey("dp-1", "default"))
 		Expect(err).ToNot(HaveOccurred())
-
-		// when
-		credential, err := issuer.Generate(id)
-
-		// then
-		Expect(err).ToNot(HaveOccurred())
-
-		// when
-		authIdentity, err := authenticator.Authenticate(context.Background(), id, credential)
-
-		// then
-		Expect(err).ToNot(HaveOccurred())
-		Expect(authIdentity.Services[0]).To(Equal("web"))
-		Expect(authIdentity.Mesh).To(Equal(id.Mesh))
 	})
 
-	It("should throw an error on invalid token", func() {
-		// when
-		id := xds.ProxyId{
+	DescribeTable("should correctly authenticate dataplane",
+		func(id builtin_issuer.DataplaneIdentity) {
+			// when
+			credential, err := issuer.Generate(id)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+
+			// when
+			authIdentity, err := authenticator.Authenticate(context.Background(), xds.ProxyId{
+				Mesh: "default",
+				Name: "dp-1",
+			}, credential)
+
+			// then
+			Expect(err).ToNot(HaveOccurred())
+			Expect(authIdentity.Services[0]).To(Equal("web"))
+			Expect(authIdentity.Mesh).To(Equal("default"))
+		},
+		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",
-			Name: "dp1",
-		}
-		_, err := authenticator.Authenticate(context.Background(), id, "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 on token with different name", func() {
-		// when
-		generateId := xds.ProxyId{
+		}),
+		Entry("should auth with token bound to mesh and name", builtin_issuer.DataplaneIdentity{
+			Name: "dp-1",
 			Mesh: "default",
-			Name: "different-name-than-dp1",
-		}
-		token, err := issuer.Generate(generateId)
-
-		// then
-		Expect(err).ToNot(HaveOccurred())
-
-		// when
-		authId := xds.ProxyId{
+		}),
+		Entry("should auth with token bound to mesh and tags", builtin_issuer.DataplaneIdentity{
 			Mesh: "default",
-			Name: "dp1",
-		}
-		_, err = authenticator.Authenticate(context.Background(), authId, token)
-
-		// then
-		Expect(err).To(MatchError("proxy name from requestor: dp1 is different than in token: different-name-than-dp1"))
-	})
-
-	It("should throw an error on token with different mesh", func() {
-		// when
-		generateId := xds.ProxyId{
-			Mesh: "different-mesh-than-default",
-			Name: "dp1",
-		}
-		token, err := issuer.Generate(generateId)
-
-		// then
-		Expect(err).ToNot(HaveOccurred())
+			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
+			authId := xds.ProxyId{
+				Mesh: "default",
+				Name: "dp-1",
+			}
+			_, err = authenticator.Authenticate(context.Background(), authId, 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: `dataplane contains tag "kuma.io/service" with value "web" 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
-		authId := xds.ProxyId{
+		id := xds.ProxyId{
 			Mesh: "default",
-			Name: "dp1",
+			Name: "dp-1",
 		}
-		_, err = authenticator.Authenticate(context.Background(), authId, token)
+		_, err := authenticator.Authenticate(context.Background(), id, "this-is-not-valid-jwt-token")
 
 		// then
-		Expect(err).To(MatchError("proxy mesh from requestor: default is different than in token: different-mesh-than-default"))
+		Expect(err).To(MatchError("could not parse token: token contains an invalid number of segments"))
 	})
 
 	It("should throw an error when dataplane is not present in CP", func() {
 		// given
-		id := xds.ProxyId{
+		id := builtin_issuer.DataplaneIdentity{
 			Mesh: "default",
 			Name: "non-existent-dp",
 		}
@@ -144,7 +200,10 @@ var _ = Describe("Authentication flow", func() {
 		Expect(err).ToNot(HaveOccurred())
 
 		// when
-		_, err = authenticator.Authenticate(context.Background(), id, token)
+		_, err = authenticator.Authenticate(context.Background(), xds.ProxyId{
+			Mesh: "default",
+			Name: "non-existent-dp",
+		}, 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 98ab28744..5e00eb9d7 100644
--- a/pkg/sds/server/server_test.go
+++ b/pkg/sds/server/server_test.go
@@ -18,9 +18,7 @@ import (
 	"github.com/kumahq/kuma/pkg/core"
 	mesh_core "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	core_manager "github.com/kumahq/kuma/pkg/core/resources/manager"
-	core_model "github.com/kumahq/kuma/pkg/core/resources/model"
 	core_store "github.com/kumahq/kuma/pkg/core/resources/store"
-	core_xds "github.com/kumahq/kuma/pkg/core/xds"
 	sds_auth "github.com/kumahq/kuma/pkg/sds/auth"
 	"github.com/kumahq/kuma/pkg/sds/server"
 	"github.com/kumahq/kuma/pkg/test"
@@ -116,7 +114,10 @@ var _ = Describe("SDS Server", func() {
 		// retrieve example DP token
 		tokenIssuer, err := tokens_builtin.NewDataplaneTokenIssuer(runtime)
 		Expect(err).ToNot(HaveOccurred())
-		dpCredential, err = tokenIssuer.Generate(core_xds.FromResourceKey(core_model.MetaToResourceKey(dpRes.GetMeta())))
+		dpCredential, err = tokenIssuer.Generate(tokens_issuer.DataplaneIdentity{
+			Name: dpRes.GetMeta().GetName(),
+			Mesh: dpRes.GetMeta().GetMesh(),
+		})
 		Expect(err).ToNot(HaveOccurred())
 
 		// start the runtime
@@ -289,7 +290,7 @@ var _ = Describe("SDS Server", func() {
 		_, err = stream.Recv()
 
 		// then
-		Expect(err).To(MatchError("rpc error: code = Unknown desc = could not parse token: token contains an invalid number of segments"))
+		Expect(err).To(MatchError("rpc error: code = Unknown desc = authentication failed: could not parse token: token contains an invalid number of segments"))
 
 		close(done)
 	}, 10)
diff --git a/pkg/tokens/builtin/server/webservice_test.go b/pkg/tokens/builtin/server/webservice_test.go
index 4b9c72e82..95855e4f2 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/core/xds"
 	"github.com/kumahq/kuma/pkg/sds/auth"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/issuer"
 	"github.com/kumahq/kuma/pkg/tokens/builtin/server"
@@ -28,12 +27,12 @@ type staticTokenIssuer struct {
 
 var _ issuer.DataplaneTokenIssuer = &staticTokenIssuer{}
 
-func (s *staticTokenIssuer) Generate(proxyId xds.ProxyId) (auth.Credential, error) {
+func (s *staticTokenIssuer) Generate(identity issuer.DataplaneIdentity) (auth.Credential, error) {
 	return auth.Credential(s.resp), nil
 }
 
-func (s *staticTokenIssuer) Validate(credential auth.Credential) (xds.ProxyId, error) {
-	return xds.ProxyId{}, errors.New("not implemented")
+func (s *staticTokenIssuer) Validate(credential auth.Credential) (issuer.DataplaneIdentity, error) {
+	return issuer.DataplaneIdentity{}, errors.New("not implemented")
 }
 
 var _ = Describe("Dataplane Token Webservice", func() {
@@ -97,8 +96,6 @@ var _ = Describe("Dataplane Token Webservice", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(resp.StatusCode).To(Equal(400))
 		},
-		Entry("json does not contain name", `{"mesh": "default"}`),
-		Entry("json does not contain mesh", `{"name": "default"}`),
 		Entry("not valid json", `not-valid-json`),
 	)
 })