Fail to join trusted cluster when leaf v9 and root v10 (#11877)

* Send Teleport version when adding remote cluster

* Send Database CA only if remote cluster is v10+

* Fix existing test and new one for v9 trusted CA validation

* Add DatabaseCAMinVersion constant and replace hardcoded versions.
This commit is contained in:
Jakub Nyckowski 2022-04-18 20:04:35 -04:00 committed by GitHub
parent ccd92cf4c2
commit b186f3a666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 198 additions and 115 deletions

View file

@ -128,6 +128,9 @@ const (
// NoLoginPrefix is the prefix used for nologin certificate principals.
NoLoginPrefix = "-teleport-nologin-"
// DatabaseCAMinVersion is the minimum Teleport version that supports Database Certificate Authority.
DatabaseCAMinVersion = "10.0.0"
)
// SystemConnectors lists the names of the system-reserved connectors.

View file

@ -25,6 +25,7 @@ import (
"time"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib"
@ -249,8 +250,9 @@ func (a *Server) establishTrust(ctx context.Context, trustedCluster types.Truste
// create a request to validate a trusted cluster (token and local certificate authorities)
validateRequest := ValidateTrustedClusterRequest{
Token: trustedCluster.GetToken(),
CAs: localCertAuthorities,
Token: trustedCluster.GetToken(),
CAs: localCertAuthorities,
TeleportVersion: teleport.Version,
}
// log the local certificate authorities that we are sending
@ -520,18 +522,11 @@ func (a *Server) validateTrustedCluster(ctx context.Context, validateRequest *Va
}
// export local cluster certificate authority and return it to the cluster
validateResponse := ValidateTrustedClusterResponse{
CAs: []types.CertAuthority{},
}
for _, caType := range []types.CertAuthType{types.HostCA, types.UserCA, types.DatabaseCA} {
certAuthority, err := a.GetCertAuthority(
ctx,
types.CertAuthID{Type: caType, DomainName: domainName},
false)
if err != nil {
return nil, trace.Wrap(err)
}
validateResponse.CAs = append(validateResponse.CAs, certAuthority)
validateResponse := ValidateTrustedClusterResponse{}
validateResponse.CAs, err = getLeafClusterCAs(ctx, a, domainName, validateRequest)
if err != nil {
return nil, trace.Wrap(err)
}
// log the local certificate authorities we are sending
@ -540,6 +535,54 @@ func (a *Server) validateTrustedCluster(ctx context.Context, validateRequest *Va
return &validateResponse, nil
}
// getLeafClusterCAs returns a slice with Cert Authorities that should be returned in response to ValidateTrustedClusterRequest.
func getLeafClusterCAs(ctx context.Context, srv *Server, domainName string, validateRequest *ValidateTrustedClusterRequest) ([]types.CertAuthority, error) {
certTypes, err := getCATypesForLeaf(validateRequest)
if err != nil {
return nil, trace.Wrap(err)
}
caCerts := make([]types.CertAuthority, 0, len(certTypes))
for _, caType := range certTypes {
certAuthority, err := srv.GetCertAuthority(
ctx,
types.CertAuthID{Type: caType, DomainName: domainName},
false)
if err != nil {
return nil, trace.Wrap(err)
}
caCerts = append(caCerts, certAuthority)
}
return caCerts, nil
}
// getCATypesForLeaf returns the list of CA certificates that should be sync in response to ValidateTrustedClusterRequest.
func getCATypesForLeaf(validateRequest *ValidateTrustedClusterRequest) ([]types.CertAuthType, error) {
var err error
databaseCASupported := false
if validateRequest.TeleportVersion != "" {
// (*ValidateTrustedClusterRequest).TeleportVersion was added in Teleport 10.0. If the request comes from an older
// cluster this field will be empty.
databaseCASupported, err = utils.MinVerWithoutPreRelease(validateRequest.TeleportVersion, constants.DatabaseCAMinVersion)
if err != nil {
return nil, trace.Wrap(err, "failed to parse Teleport version: %q", validateRequest.TeleportVersion)
}
}
certTypes := []types.CertAuthType{types.HostCA, types.UserCA}
if databaseCASupported {
// Database CA was introduced in Teleport 10.0. Do not send it to older clusters
// as they don't understand it.
certTypes = append(certTypes, types.DatabaseCA)
}
return certTypes, nil
}
func (a *Server) validateTrustedClusterToken(ctx context.Context, tokenName string) (map[string]string, error) {
provisionToken, err := a.ValidateToken(ctx, tokenName)
if err != nil {
@ -614,8 +657,9 @@ func (a *Server) sendValidateRequestToProxy(host string, validateRequest *Valida
}
type ValidateTrustedClusterRequest struct {
Token string `json:"token"`
CAs []types.CertAuthority `json:"certificate_authorities"`
Token string `json:"token"`
CAs []types.CertAuthority `json:"certificate_authorities"`
TeleportVersion string `json:"teleport_version"`
}
func (v *ValidateTrustedClusterRequest) ToRaw() (*ValidateTrustedClusterRequestRaw, error) {
@ -631,14 +675,16 @@ func (v *ValidateTrustedClusterRequest) ToRaw() (*ValidateTrustedClusterRequestR
}
return &ValidateTrustedClusterRequestRaw{
Token: v.Token,
CAs: cas,
Token: v.Token,
CAs: cas,
TeleportVersion: v.TeleportVersion,
}, nil
}
type ValidateTrustedClusterRequestRaw struct {
Token string `json:"token"`
CAs [][]byte `json:"certificate_authorities"`
Token string `json:"token"`
CAs [][]byte `json:"certificate_authorities"`
TeleportVersion string `json:"teleport_version"`
}
func (v *ValidateTrustedClusterRequestRaw) ToNative() (*ValidateTrustedClusterRequest, error) {
@ -654,8 +700,9 @@ func (v *ValidateTrustedClusterRequestRaw) ToNative() (*ValidateTrustedClusterRe
}
return &ValidateTrustedClusterRequest{
Token: v.Token,
CAs: cas,
Token: v.Token,
CAs: cas,
TeleportVersion: v.TeleportVersion,
}, nil
}

View file

@ -123,113 +123,145 @@ func TestValidateTrustedCluster(t *testing.T) {
err = a.SetStaticTokens(tks)
require.NoError(t, err)
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: "invalidtoken",
CAs: []types.CertAuthority{},
t.Run("invalid cluster token", func(t *testing.T) {
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: "invalidtoken",
CAs: []types.CertAuthority{},
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid cluster token")
})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid cluster token")
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{},
t.Run("missing CA", func(t *testing.T) {
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{},
})
require.Error(t, err)
require.Contains(t, err.Error(), "expected exactly one")
})
require.Error(t, err)
require.Contains(t, err.Error(), "expected exactly one")
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.HostCA, "rc1"),
suite.NewTestCA(types.HostCA, "rc2"),
},
t.Run("more than one CA", func(t *testing.T) {
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.HostCA, "rc1"),
suite.NewTestCA(types.HostCA, "rc2"),
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "expected exactly one")
})
require.Error(t, err)
require.Contains(t, err.Error(), "expected exactly one")
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.UserCA, "rc3"),
},
t.Run("wrong CA type", func(t *testing.T) {
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.UserCA, "rc3"),
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "expected host certificate authority")
})
require.Error(t, err)
require.Contains(t, err.Error(), "expected host certificate authority")
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.HostCA, localClusterName),
},
t.Run("wrong CA name", func(t *testing.T) {
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.HostCA, localClusterName),
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "same name as this cluster")
})
require.Error(t, err)
require.Contains(t, err.Error(), "same name as this cluster")
trustedCluster, err := types.NewTrustedCluster("trustedcluster",
types.TrustedClusterSpecV2{Roles: []string{"nonempty"}})
require.NoError(t, err)
// use the UpsertTrustedCluster in Presence as we just want the resource in
// the backend, we don't want to actually connect
_, err = a.Presence.UpsertTrustedCluster(ctx, trustedCluster)
require.NoError(t, err)
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.HostCA, trustedCluster.GetName()),
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "same name as trusted cluster")
leafClusterCA := types.CertAuthority(suite.NewTestCA(types.HostCA, "leafcluster"))
resp, err := a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{leafClusterCA},
})
require.NoError(t, err)
require.Len(t, resp.CAs, 3)
require.ElementsMatch(t,
[]types.CertAuthType{types.HostCA, types.UserCA, types.DatabaseCA},
[]types.CertAuthType{resp.CAs[0].GetType(), resp.CAs[1].GetType(), resp.CAs[2].GetType()},
)
for _, returnedCA := range resp.CAs {
localCA, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: returnedCA.GetType(),
DomainName: localClusterName,
}, false)
t.Run("wrong remote CA name", func(t *testing.T) {
trustedCluster, err := types.NewTrustedCluster("trustedcluster",
types.TrustedClusterSpecV2{Roles: []string{"nonempty"}})
require.NoError(t, err)
// use the UpsertTrustedCluster in Presence as we just want the resource in
// the backend, we don't want to actually connect
_, err = a.Presence.UpsertTrustedCluster(ctx, trustedCluster)
require.NoError(t, err)
require.True(t, services.CertAuthoritiesEquivalent(localCA, returnedCA))
}
rcs, err := a.GetRemoteClusters()
require.NoError(t, err)
require.Len(t, rcs, 1)
require.Equal(t, leafClusterCA.GetName(), rcs[0].GetName())
_, err = a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{
suite.NewTestCA(types.HostCA, trustedCluster.GetName()),
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "same name as trusted cluster")
hostCAs, err := a.GetCertAuthorities(ctx, types.HostCA, false)
require.NoError(t, err)
require.Len(t, hostCAs, 2)
require.ElementsMatch(t,
[]string{localClusterName, leafClusterCA.GetName()},
[]string{hostCAs[0].GetName(), hostCAs[1].GetName()},
)
require.Empty(t, hostCAs[0].GetRoles())
require.Empty(t, hostCAs[0].GetRoleMap())
require.Empty(t, hostCAs[1].GetRoles())
require.Empty(t, hostCAs[1].GetRoleMap())
})
userCAs, err := a.GetCertAuthorities(ctx, types.UserCA, false)
require.NoError(t, err)
require.Len(t, userCAs, 1)
require.Equal(t, localClusterName, userCAs[0].GetName())
t.Run("all CAs are returned when v10+", func(t *testing.T) {
leafClusterCA := types.CertAuthority(suite.NewTestCA(types.HostCA, "leafcluster"))
resp, err := a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{leafClusterCA},
TeleportVersion: teleport.Version,
})
require.NoError(t, err)
dbCAs, err := a.GetCertAuthorities(ctx, types.DatabaseCA, false)
require.NoError(t, err)
require.Len(t, dbCAs, 1)
require.Equal(t, localClusterName, dbCAs[0].GetName())
require.Len(t, resp.CAs, 3)
require.ElementsMatch(t,
[]types.CertAuthType{types.HostCA, types.UserCA, types.DatabaseCA},
[]types.CertAuthType{resp.CAs[0].GetType(), resp.CAs[1].GetType(), resp.CAs[2].GetType()},
)
for _, returnedCA := range resp.CAs {
localCA, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: returnedCA.GetType(),
DomainName: localClusterName,
}, false)
require.NoError(t, err)
require.True(t, services.CertAuthoritiesEquivalent(localCA, returnedCA))
}
rcs, err := a.GetRemoteClusters()
require.NoError(t, err)
require.Len(t, rcs, 1)
require.Equal(t, leafClusterCA.GetName(), rcs[0].GetName())
hostCAs, err := a.GetCertAuthorities(ctx, types.HostCA, false)
require.NoError(t, err)
require.Len(t, hostCAs, 2)
require.ElementsMatch(t,
[]string{localClusterName, leafClusterCA.GetName()},
[]string{hostCAs[0].GetName(), hostCAs[1].GetName()},
)
require.Empty(t, hostCAs[0].GetRoles())
require.Empty(t, hostCAs[0].GetRoleMap())
require.Empty(t, hostCAs[1].GetRoles())
require.Empty(t, hostCAs[1].GetRoleMap())
userCAs, err := a.GetCertAuthorities(ctx, types.UserCA, false)
require.NoError(t, err)
require.Len(t, userCAs, 1)
require.Equal(t, localClusterName, userCAs[0].GetName())
dbCAs, err := a.GetCertAuthorities(ctx, types.DatabaseCA, false)
require.NoError(t, err)
require.Len(t, dbCAs, 1)
require.Equal(t, localClusterName, dbCAs[0].GetName())
})
t.Run("only Host and User CA are returned for v9", func(t *testing.T) {
leafClusterCA := types.CertAuthority(suite.NewTestCA(types.HostCA, "leafcluster"))
resp, err := a.validateTrustedCluster(ctx, &ValidateTrustedClusterRequest{
Token: validToken,
CAs: []types.CertAuthority{leafClusterCA},
TeleportVersion: "",
})
require.NoError(t, err)
require.Len(t, resp.CAs, 2)
require.ElementsMatch(t,
[]types.CertAuthType{types.HostCA, types.UserCA},
[]types.CertAuthType{resp.CAs[0].GetType(), resp.CAs[1].GetType()},
)
})
}
func newTestAuthServer(ctx context.Context, t *testing.T, name ...string) *Server {

View file

@ -31,6 +31,7 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
@ -646,7 +647,7 @@ func (s *ProxyServer) getConfigForServer(ctx context.Context, identity tlsca.Ide
// DatabaseCA was introduced in Teleport 10. Older versions require database certificate signed
// with UserCA where Teleport 10+ uses DatabaseCA.
ver10orAbove, err := utils.MinVerWithoutPreRelease(server.GetTeleportVersion(), "10.0.0")
ver10orAbove, err := utils.MinVerWithoutPreRelease(server.GetTeleportVersion(), constants.DatabaseCAMinVersion)
if err != nil {
return nil, trace.Wrap(err, "failed to parse Teleport version: %q", server.GetTeleportVersion())
}