mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 14:59:53 +08:00
Converts the backend.Key from a slice of bytes to a concrete struct. The main motivation behind this change is to be able to better distinguish individual components of a key. The textual representation of a backend key is constructed by joining all components of the key with a /. By representing the entire key as a single textual object it prevented resources containing a / in their name from being properly identified. There was a lot of code that interpolated keys in various manners and expected only a specific number of subcomponents for a particular resource. This lead to most, if not all, of the RPCs used to create a resource to permit a name containing /, (i.e. a user named test/llama/1), but any RPCs used to retrieve the resources would fail to retrieve them from the backend. Every backend.Key now carries a slice of all the individual components in addition to the textual representation. This permits more fine grained inspection per component, while also not having to construct the textual representation for a group of components more than once. The backend.Sanitizer was updated to only validate keys for backend writes. Retrieving and deleting invalid of malformed keys is now permitted. This will allow any existing resources that were created with / in one of the components to become retrievable and deletable. However, any new resources with / in their name are explicitly prevented. Closes #6088 Closes #9107 Closes #10576 Closes #42823
482 lines
14 KiB
Go
482 lines
14 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/trace"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
|
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
apievents "github.com/gravitational/teleport/api/types/events"
|
|
"github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth/join"
|
|
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
|
|
"github.com/gravitational/teleport/lib/auth/state"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
func TestAuth_RegisterUsingToken(t *testing.T) {
|
|
ctx := context.Background()
|
|
p, err := newTestPack(ctx, t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// create a static token
|
|
staticToken := types.ProvisionTokenV1{
|
|
Roles: []types.SystemRole{types.RoleNode},
|
|
Token: "static_token",
|
|
}
|
|
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
|
|
StaticTokens: []types.ProvisionTokenV1{staticToken},
|
|
})
|
|
require.NoError(t, err)
|
|
err = p.a.SetStaticTokens(staticTokens)
|
|
require.NoError(t, err)
|
|
|
|
// create a valid dynamic token
|
|
dynamicToken := generateTestToken(
|
|
ctx,
|
|
t,
|
|
types.SystemRoles{types.RoleNode},
|
|
p.a.GetClock().Now().Add(time.Minute*30),
|
|
p.a,
|
|
)
|
|
|
|
// create an expired dynamic token
|
|
expiredDynamicToken := generateTestToken(
|
|
ctx,
|
|
t,
|
|
types.SystemRoles{types.RoleNode},
|
|
p.a.GetClock().Now().Add(-time.Minute*30),
|
|
p.a,
|
|
)
|
|
|
|
sshPrivateKey, sshPublicKey, err := testauthority.New().GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
|
|
tlsPublicKey, err := PrivateKeyToPublicKeyTLS(sshPrivateKey)
|
|
require.NoError(t, err)
|
|
|
|
testcases := []struct {
|
|
desc string
|
|
req *types.RegisterUsingTokenRequest
|
|
certsAssertion func(*proto.Certs)
|
|
errorAssertion func(error) bool
|
|
waitTokenDeleted bool // Expired tokens are deleted in background, might need slight delay in relevant test
|
|
}{
|
|
{
|
|
desc: "reject empty",
|
|
req: &types.RegisterUsingTokenRequest{},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "reject no token",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "reject no HostID",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: staticToken.Token,
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "allow no NodeName",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: staticToken.Token,
|
|
HostID: "localhost",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
},
|
|
{
|
|
desc: "reject no SSH pub",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: staticToken.Token,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "reject no TLS pub",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: staticToken.Token,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "reject bad token",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: "not a token",
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsAccessDenied,
|
|
},
|
|
{
|
|
desc: "allow static token",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: staticToken.Token,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
},
|
|
{
|
|
desc: "reject wrong role static",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: staticToken.Token,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleProxy,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "allow dynamic token",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: dynamicToken,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
},
|
|
{
|
|
desc: "reject wrong role dynamic",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: dynamicToken,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleProxy,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsBadParameter,
|
|
},
|
|
{
|
|
desc: "check additional principals",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: dynamicToken,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
AdditionalPrincipals: []string{"example.com"},
|
|
},
|
|
certsAssertion: func(certs *proto.Certs) {
|
|
hostCert, err := sshutils.ParseCertificate(certs.SSH)
|
|
require.NoError(t, err)
|
|
require.Contains(t, hostCert.ValidPrincipals, "example.com")
|
|
},
|
|
},
|
|
{
|
|
desc: "reject expired dynamic token",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: expiredDynamicToken,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
waitTokenDeleted: true,
|
|
errorAssertion: trace.IsAccessDenied,
|
|
},
|
|
{
|
|
// relies on token being deleted during previous testcase
|
|
desc: "expired token should be gone",
|
|
req: &types.RegisterUsingTokenRequest{
|
|
Token: expiredDynamicToken,
|
|
HostID: "localhost",
|
|
NodeName: "node-name",
|
|
Role: types.RoleNode,
|
|
PublicSSHKey: sshPublicKey,
|
|
PublicTLSKey: tlsPublicKey,
|
|
},
|
|
errorAssertion: trace.IsAccessDenied,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
certs, err := p.a.RegisterUsingToken(ctx, tc.req)
|
|
if tc.errorAssertion != nil {
|
|
require.True(t, tc.errorAssertion(err))
|
|
if tc.waitTokenDeleted {
|
|
require.Eventually(t, func() bool {
|
|
_, err := p.a.ValidateToken(ctx, tc.req.Token)
|
|
return err != nil && strings.Contains(err.Error(), TokenExpiredOrNotFound)
|
|
}, time.Millisecond*100, time.Millisecond*10)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
if tc.certsAssertion != nil {
|
|
tc.certsAssertion(certs)
|
|
}
|
|
|
|
// Check audit log event is emitted
|
|
evt := p.mockEmitter.LastEvent()
|
|
require.NotNil(t, evt)
|
|
joinEvent, ok := evt.(*apievents.InstanceJoin)
|
|
require.True(t, ok)
|
|
require.Equal(t, events.InstanceJoinEvent, joinEvent.Type)
|
|
require.Equal(t, events.InstanceJoinCode, joinEvent.Code)
|
|
require.Equal(t, tc.req.NodeName, joinEvent.NodeName)
|
|
require.Equal(t, tc.req.HostID, joinEvent.HostID)
|
|
require.EqualValues(t, tc.req.Role, joinEvent.Role)
|
|
require.EqualValues(t, types.JoinMethodToken, joinEvent.Method)
|
|
})
|
|
}
|
|
}
|
|
|
|
func newBotToken(t *testing.T, tokenName, botName string, role types.SystemRole, expiry time.Time) types.ProvisionToken {
|
|
t.Helper()
|
|
token, err := types.NewProvisionTokenFromSpec(tokenName, expiry, types.ProvisionTokenSpecV2{
|
|
Roles: []types.SystemRole{role},
|
|
BotName: botName,
|
|
})
|
|
require.NoError(t, err, "could not create bot token")
|
|
return token
|
|
}
|
|
|
|
// TestRegister_Bot tests that a provision token can be used to generate
|
|
// renewable certificates for a non-interactive user.
|
|
func TestRegister_Bot(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
srv := newTestTLSServer(t)
|
|
|
|
bot, err := machineidv1.UpsertBot(ctx, srv.Auth(), &machineidv1pb.Bot{
|
|
Metadata: &headerv1.Metadata{
|
|
Name: "test",
|
|
},
|
|
Spec: &machineidv1pb.BotSpec{
|
|
Roles: []string{},
|
|
},
|
|
}, srv.Clock().Now(), "")
|
|
require.NoError(t, err)
|
|
|
|
later := srv.Clock().Now().Add(4 * time.Hour)
|
|
|
|
goodToken := newBotToken(t, "good-token", bot.Metadata.Name, types.RoleBot, later)
|
|
expiredToken := newBotToken(t, "expired", bot.Metadata.Name, types.RoleBot, srv.Clock().Now().Add(-1*time.Hour))
|
|
wrongKind := newBotToken(t, "wrong-kind", "", types.RoleNode, later)
|
|
wrongUser := newBotToken(t, "wrong-user", "llama", types.RoleBot, later)
|
|
invalidToken := newBotToken(t, "this-token-does-not-exist", bot.Metadata.Name, types.RoleBot, later)
|
|
|
|
err = srv.Auth().UpsertToken(ctx, goodToken)
|
|
require.NoError(t, err)
|
|
err = srv.Auth().UpsertToken(ctx, expiredToken)
|
|
require.NoError(t, err)
|
|
err = srv.Auth().UpsertToken(ctx, wrongKind)
|
|
require.NoError(t, err)
|
|
err = srv.Auth().UpsertToken(ctx, wrongUser)
|
|
require.NoError(t, err)
|
|
|
|
for _, test := range []struct {
|
|
desc string
|
|
token types.ProvisionToken
|
|
assertErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
desc: "OK good token",
|
|
token: goodToken,
|
|
assertErr: require.NoError,
|
|
},
|
|
{
|
|
desc: "NOK expired token",
|
|
token: expiredToken,
|
|
assertErr: require.Error,
|
|
},
|
|
{
|
|
desc: "NOK wrong token kind",
|
|
token: wrongKind,
|
|
assertErr: require.Error,
|
|
},
|
|
{
|
|
desc: "NOK token for wrong user",
|
|
token: wrongUser,
|
|
assertErr: require.Error,
|
|
},
|
|
{
|
|
desc: "NOK invalid token",
|
|
token: invalidToken,
|
|
assertErr: require.Error,
|
|
},
|
|
} {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
start := srv.Clock().Now()
|
|
result, err := join.Register(ctx, join.RegisterParams{
|
|
Token: test.token.GetName(),
|
|
ID: state.IdentityID{
|
|
Role: types.RoleBot,
|
|
},
|
|
AuthServers: []utils.NetAddr{*utils.MustParseAddr(srv.Addr().String())},
|
|
})
|
|
test.assertErr(t, err)
|
|
|
|
if err == nil {
|
|
require.NotEmpty(t, result.Certs.SSH)
|
|
require.NotEmpty(t, result.Certs.TLS)
|
|
|
|
// ensure token was removed
|
|
_, err = srv.Auth().GetToken(ctx, test.token.GetName())
|
|
require.True(t, trace.IsNotFound(err), "expected not found error, got %v", err)
|
|
|
|
// ensure cert is renewable
|
|
x509, err := tlsca.ParseCertificatePEM(result.Certs.TLS)
|
|
require.NoError(t, err)
|
|
id, err := tlsca.FromSubject(x509.Subject, later)
|
|
require.NoError(t, err)
|
|
require.True(t, id.Renewable)
|
|
|
|
// Check audit event
|
|
evts, _, err := srv.Auth().SearchEvents(ctx, events.SearchEventsRequest{
|
|
From: start,
|
|
To: srv.Clock().Now(),
|
|
EventTypes: []string{events.BotJoinEvent},
|
|
Limit: 1,
|
|
Order: types.EventOrderDescending,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, evts, 1)
|
|
evt, ok := evts[0].(*apievents.BotJoin)
|
|
require.True(t, ok)
|
|
require.Equal(t, events.BotJoinEvent, evt.Type)
|
|
require.Equal(t, events.BotJoinCode, evt.Code)
|
|
require.EqualValues(t, types.JoinMethodToken, evt.Method)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRegister_Bot_Expiry checks that bot certificate expiry can be set, and
|
|
// does not exceed the limit.
|
|
func TestRegister_Bot_Expiry(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
srv := newTestTLSServer(t)
|
|
|
|
validExpires := srv.Clock().Now().Add(time.Hour * 6)
|
|
tooGreatExpires := srv.Clock().Now().Add(time.Hour * 24 * 365)
|
|
tests := []struct {
|
|
name string
|
|
requestExpires *time.Time
|
|
expectTTL time.Duration
|
|
}{
|
|
{
|
|
name: "unspecified defaults",
|
|
requestExpires: nil,
|
|
expectTTL: defaults.DefaultRenewableCertTTL,
|
|
},
|
|
{
|
|
name: "valid value specified",
|
|
requestExpires: &validExpires,
|
|
expectTTL: time.Hour * 6,
|
|
},
|
|
{
|
|
name: "value exceeding limit specified",
|
|
requestExpires: &tooGreatExpires,
|
|
// MaxSessionTTL set in createBotRole is 12 hours, so this cap will
|
|
// apply instead of the defaults.MaxRenewableCertTTL specified
|
|
// in generateInitialBotCerts.
|
|
expectTTL: 12 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
botName := uuid.NewString()
|
|
_, err := machineidv1.UpsertBot(ctx, srv.Auth(), &machineidv1pb.Bot{
|
|
Metadata: &headerv1.Metadata{
|
|
Name: botName,
|
|
},
|
|
Spec: &machineidv1pb.BotSpec{
|
|
Roles: []string{},
|
|
Traits: []*machineidv1pb.Trait{},
|
|
},
|
|
}, srv.Clock().Now(), "")
|
|
require.NoError(t, err)
|
|
tok := newBotToken(t, uuid.NewString(), botName, types.RoleBot, srv.Clock().Now().Add(time.Hour))
|
|
require.NoError(t, srv.Auth().UpsertToken(ctx, tok))
|
|
|
|
result, err := join.Register(ctx, join.RegisterParams{
|
|
Token: tok.GetName(),
|
|
ID: state.IdentityID{
|
|
Role: types.RoleBot,
|
|
},
|
|
AuthServers: []utils.NetAddr{*utils.MustParseAddr(srv.Addr().String())},
|
|
Expires: tt.requestExpires,
|
|
})
|
|
require.NoError(t, err)
|
|
x509, err := tlsca.ParseCertificatePEM(result.Certs.TLS)
|
|
require.NoError(t, err)
|
|
id, err := tlsca.FromSubject(x509.Subject, x509.NotAfter)
|
|
require.NoError(t, err)
|
|
|
|
ttl := id.Expires.Sub(srv.Clock().Now())
|
|
require.Equal(t, tt.expectTTL, ttl)
|
|
})
|
|
}
|
|
}
|