mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
MWI: Bound Keypair - Registration Secrets (#55380)
* MWI: Bound Keypair - Registration Secrets This adds support for initial joining via registration secrets. These one time use secrets emulate traditional token joining and allow clients to perform their initial join With this, no options are required for bound keypair-type tokens. While admins can specify a joining secret if they wish, if none is provided, one will be generated on the server and can be found in `status.bound_keypair.registration_secret` on the token resource. When joining, this secret can be shared with clients in addition to the (no longer sensitive) token name. This secret is verified and a keypair rotation is requested, prompting the client to generate a new keypair, provide the public key to the server, and complete a joining challenge. It then joins the cluster as usual. * Remove unnecessary token validation checks * Rename tbot flag to --registration-secret * Fix reference to renamed flag * Various fixes, mostly more unwanted checks * Add test cases for registration secrets * Fix broken test Onboarding config is no longer required, so fix the now-broken test * Allow empty .spec.bound_keypair field for bound keypair tokens This allows .spec.bound_keypair to be empty or entirely unset, since we can build defaults at creation time. * Add test for secret expiry enforcement * Handle nonexistent client state when using a registration secret * Fix test lints * Hide exact registration secret rejection reason from client Registration secret errors now return a single error message to the client and log a more specific message on the server.
This commit is contained in:
@@ -441,15 +441,11 @@ func (p *ProvisionTokenV2) CheckAndSetDefaults() error {
|
||||
return trace.Wrap(err, "spec.azure_devops: failed validation")
|
||||
}
|
||||
case JoinMethodBoundKeypair:
|
||||
providerCfg := p.Spec.BoundKeypair
|
||||
if providerCfg == nil {
|
||||
return trace.BadParameter(
|
||||
"spec.bound_keypair: must be configured for the join method %q",
|
||||
JoinMethodBoundKeypair,
|
||||
)
|
||||
if p.Spec.BoundKeypair == nil {
|
||||
p.Spec.BoundKeypair = &ProvisionTokenSpecV2BoundKeypair{}
|
||||
}
|
||||
|
||||
if err := providerCfg.checkAndSetDefaults(); err != nil {
|
||||
if err := p.Spec.BoundKeypair.checkAndSetDefaults(); err != nil {
|
||||
return trace.Wrap(err, "spec.bound_keypair: failed validation")
|
||||
}
|
||||
default:
|
||||
@@ -1032,22 +1028,16 @@ func (a *ProvisionTokenSpecV2AzureDevops) checkAndSetDefaults() error {
|
||||
}
|
||||
|
||||
func (a *ProvisionTokenSpecV2BoundKeypair) checkAndSetDefaults() error {
|
||||
// Note: don't attempt to initialize onboarding - at least for now - as it
|
||||
// has required keys. This behavior may be relaxed when we add
|
||||
// server-generated joining secrets.
|
||||
if a.Onboarding == nil {
|
||||
return trace.BadParameter("spec.bound_keypair.onboarding is required")
|
||||
}
|
||||
|
||||
if a.Onboarding.RegistrationSecret == "" && a.Onboarding.InitialPublicKey == "" {
|
||||
return trace.BadParameter("at least one of [initial_join_secret, " +
|
||||
"initial_public_key] is required in spec.bound_keypair.onboarding")
|
||||
a.Onboarding = &ProvisionTokenSpecV2BoundKeypair_OnboardingSpec{}
|
||||
}
|
||||
|
||||
if a.Recovery == nil {
|
||||
a.Recovery = &ProvisionTokenSpecV2BoundKeypair_RecoverySpec{}
|
||||
}
|
||||
|
||||
// Limit must be >= 1 for the token to be useful. If zero, assume it's unset
|
||||
// and provide a sane default.
|
||||
if a.Recovery.Limit == 0 {
|
||||
a.Recovery.Limit = 1
|
||||
}
|
||||
|
||||
@@ -1408,6 +1408,8 @@ func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
// note: missing onboarding config is allowed; we'll generate some
|
||||
// fields at creation/upsert time.
|
||||
desc: "bound keypair missing onboarding config",
|
||||
token: &ProvisionTokenV2{
|
||||
Metadata: Metadata{
|
||||
@@ -1419,7 +1421,7 @@ func TestProvisionTokenV2_CheckAndSetDefaults(t *testing.T) {
|
||||
BoundKeypair: &ProvisionTokenSpecV2BoundKeypair{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -183,24 +183,26 @@ func (c *ClientState) SignerForPublicKey(authorizedKeysBytes []byte) (crypto.Sig
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Check the active key first.
|
||||
activePubKeyBytes, err := c.ToPublicKeyBytes()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
equal, err := pubKeyEqual(desiredPubKey, activePubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
} else if equal {
|
||||
// Parse a fresh copy of the key since this will escape the mutex and we
|
||||
// can't be sure our local copy is thread safe.
|
||||
key, err := keys.ParsePrivateKey(c.PrivateKeyBytes)
|
||||
// Check the active key first, if available.
|
||||
if c.PrivateKey != nil {
|
||||
activePubKeyBytes, err := c.ToPublicKeyBytes()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return key.Signer, nil
|
||||
equal, err := pubKeyEqual(desiredPubKey, activePubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
} else if equal {
|
||||
// Parse a fresh copy of the key since this will escape the mutex and we
|
||||
// can't be sure our local copy is thread safe.
|
||||
key, err := keys.ParsePrivateKey(c.PrivateKeyBytes)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return key.Signer, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, search through the key history. If a keypair rotation was
|
||||
@@ -268,14 +270,16 @@ func (c *ClientState) SetActiveKey(signer crypto.Signer) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
equal, err := pubKeyEqual(signer.Public(), c.PrivateKey.Public())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if c.PrivateKey != nil {
|
||||
equal, err := pubKeyEqual(signer.Public(), c.PrivateKey.Public())
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if equal {
|
||||
// nothing to do; specified key is already the active key
|
||||
return nil
|
||||
if equal {
|
||||
// nothing to do; specified key is already the active key
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
key, err := keys.NewPrivateKey(signer)
|
||||
@@ -356,6 +360,13 @@ func LoadClientState(ctx context.Context, fs FS) (*ClientState, error) {
|
||||
return nil, trace.Wrap(err, "reading private key")
|
||||
}
|
||||
|
||||
// The private key may be empty if this is an initial join attempt using a
|
||||
// configured registration secret. This is allowed, but callers should
|
||||
// handle this via `NewEmptyClientState()`
|
||||
if len(privateKeyBytes) == 0 {
|
||||
return nil, trace.NotFound("no active private key found")
|
||||
}
|
||||
|
||||
joinStateBytes, err := fs.Read(ctx, JoinStatePath)
|
||||
if trace.IsNotFound(err) {
|
||||
// Join state doesn't exist, this is allowed.
|
||||
@@ -443,7 +454,8 @@ func (c *ClientState) Store(ctx context.Context) error {
|
||||
|
||||
// NewUnboundClientState creates a new client state that has not yet been bound,
|
||||
// i.e. a new keypair that has not been registered with Auth, and no prior join
|
||||
// state.
|
||||
// state. Join attempts using registration secrets should instead use
|
||||
// `NewEmptyClientState`, which does not immediately generate a keypair.
|
||||
func NewUnboundClientState(ctx context.Context, fs FS, getSuite cryptosuites.GetSuiteFunc) (*ClientState, error) {
|
||||
key, err := cryptosuites.GenerateKey(ctx, getSuite, cryptosuites.BoundKeypairJoining)
|
||||
if err != nil {
|
||||
@@ -483,3 +495,12 @@ func NewUnboundClientState(ctx context.Context, fs FS, getSuite cryptosuites.Get
|
||||
KeyHistory: history,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewEmptyClientState creates a new ClientState with no existing active private
|
||||
// key or key history. This is only appropriate when a registration secret
|
||||
// should be used.
|
||||
func NewEmptyClientState(fs FS) *ClientState {
|
||||
return &ClientState{
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
+142
-33
@@ -21,6 +21,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
@@ -31,8 +32,10 @@ import (
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/boundkeypair"
|
||||
"github.com/gravitational/teleport/lib/boundkeypair/boundkeypairexperiment"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/jwt"
|
||||
libsshutils "github.com/gravitational/teleport/lib/sshutils"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
type boundKeypairValidator interface {
|
||||
@@ -52,21 +55,62 @@ func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair)
|
||||
return trace.BadParameter("bound keypair joining experiment is not enabled")
|
||||
}
|
||||
|
||||
if spec.Onboarding.RegistrationSecret != "" {
|
||||
return trace.NotImplemented("spec.bound_keypair.onboarding.registration_secret is not yet implemented")
|
||||
}
|
||||
|
||||
if spec.Onboarding.InitialPublicKey == "" {
|
||||
return trace.NotImplemented("spec.bound_keypair.onboarding.initial_public_key is currently required")
|
||||
}
|
||||
|
||||
if spec.Recovery == nil {
|
||||
return trace.BadParameter("spec.recovery: field is required")
|
||||
return trace.BadParameter("spec.bound_keypair.recovery: field is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateRegistrationSecret populates the
|
||||
// `status.BoundKeypair.RegistrationSecret` field of a bound keypair token. It
|
||||
// should be called as part of any token creation or update to ensure the
|
||||
// registration secret is made available if needed.
|
||||
func populateRegistrationSecret(v2 *types.ProvisionTokenV2) error {
|
||||
if v2.GetJoinMethod() != types.JoinMethodBoundKeypair {
|
||||
return trace.BadParameter("must be called with a bound keypair token")
|
||||
}
|
||||
|
||||
if v2.Spec.BoundKeypair == nil {
|
||||
v2.Spec.BoundKeypair = &types.ProvisionTokenSpecV2BoundKeypair{}
|
||||
}
|
||||
|
||||
if v2.Status == nil {
|
||||
v2.Status = &types.ProvisionTokenStatusV2{}
|
||||
}
|
||||
if v2.Status.BoundKeypair == nil {
|
||||
v2.Status.BoundKeypair = &types.ProvisionTokenStatusV2BoundKeypair{}
|
||||
}
|
||||
|
||||
spec := v2.Spec.BoundKeypair
|
||||
status := v2.Status.BoundKeypair
|
||||
|
||||
if status.BoundPublicKey != "" || spec.Onboarding.InitialPublicKey != "" {
|
||||
// A key has already been bound or preregistered, nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
if status.RegistrationSecret != "" {
|
||||
// A secret has already been generated, nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
if spec.Onboarding.RegistrationSecret != "" {
|
||||
// An explicit registration secret was provided, so copy it to status.
|
||||
status.RegistrationSecret = spec.Onboarding.RegistrationSecret
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise, we have no key and no secret, so generate one now.
|
||||
s, err := utils.CryptoRandomHex(defaults.TokenLenBytes)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
status.RegistrationSecret = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Server) CreateBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error {
|
||||
if token.GetJoinMethod() != types.JoinMethodBoundKeypair {
|
||||
return trace.BadParameter("must be called with a bound keypair token")
|
||||
@@ -91,7 +135,9 @@ func (a *Server) CreateBoundKeypairToken(ctx context.Context, token types.Provis
|
||||
// stop that that wouldn't also break backup and restore. For now, it's
|
||||
// simpler and easier to just tell users not to edit those fields.
|
||||
|
||||
// TODO (follow up PR): Populate initial_join_secret if needed.
|
||||
if err := populateRegistrationSecret(tokenV2); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return trace.Wrap(a.CreateToken(ctx, tokenV2))
|
||||
}
|
||||
@@ -115,8 +161,9 @@ func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.Provis
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// TODO: Follow up with proper checking for a preexisting resource so
|
||||
// generated fields are handled properly, i.e. initial secret generation.
|
||||
if err := populateRegistrationSecret(tokenV2); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Implementation note: checkAndSetDefaults() impl for this token type is
|
||||
// called at insertion time as part of `tokenToItem()`
|
||||
@@ -297,11 +344,11 @@ func (a *Server) requestBoundKeypairRotation(
|
||||
// state is still sane before the update is committed.
|
||||
type boundKeypairStatusMutator func(*types.ProvisionTokenSpecV2BoundKeypair, *types.ProvisionTokenStatusV2BoundKeypair) error
|
||||
|
||||
// mutateStatusConsumeJoin consumes a "hard" join on the backend, incrementing
|
||||
// mutateStatusConsumeRecovery consumes a "hard" join on the backend, incrementing
|
||||
// the recovery counter. This verifies that the backend recovery count has not
|
||||
// changed, and that total join count is at least the value when the mutator was
|
||||
// created.
|
||||
func mutateStatusConsumeJoin(mode boundkeypair.RecoveryMode, expectRecoveryCount uint32, expectMinRecoveryLimit uint32) boundKeypairStatusMutator {
|
||||
func mutateStatusConsumeRecovery(mode boundkeypair.RecoveryMode, expectRecoveryCount uint32, expectMinRecoveryLimit uint32) boundKeypairStatusMutator {
|
||||
now := time.Now()
|
||||
|
||||
return func(spec *types.ProvisionTokenSpecV2BoundKeypair, status *types.ProvisionTokenStatusV2BoundKeypair) error {
|
||||
@@ -376,6 +423,19 @@ func mutateStatusLastRotatedAt(newValue, expectPrevValue *time.Time) boundKeypai
|
||||
}
|
||||
}
|
||||
|
||||
// mutateStatusClearRegistrationSecret clears the registration secret field to
|
||||
// prevent further join attempts using this secret.
|
||||
func mutateStatusClearRegistrationSecret(oldValue string) boundKeypairStatusMutator {
|
||||
return func(_ *types.ProvisionTokenSpecV2BoundKeypair, status *types.ProvisionTokenStatusV2BoundKeypair) error {
|
||||
if status.RegistrationSecret != oldValue {
|
||||
return trace.AccessDenied("unexpected backend state")
|
||||
}
|
||||
|
||||
status.RegistrationSecret = ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// formatTimePointer stringifies a *time.Time for logging, but gracefully
|
||||
// handles nil values.
|
||||
func formatTimePointer(t *time.Time) string {
|
||||
@@ -521,35 +581,84 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
case !hasBoundPublicKey && !hasIncomingBotInstance:
|
||||
// Normal initial join attempt. No bound key, and no incoming bot
|
||||
// instance. Consumes a recovery attempt.
|
||||
if spec.Onboarding.RegistrationSecret != "" {
|
||||
return nil, trace.NotImplemented("initial joining secrets are not yet supported")
|
||||
}
|
||||
|
||||
if spec.Onboarding.InitialPublicKey == "" {
|
||||
return nil, trace.BadParameter("an initial public key is required")
|
||||
}
|
||||
|
||||
if recoveryMode == boundkeypair.RecoveryModeStandard && !hasJoinsRemaining {
|
||||
return nil, trace.AccessDenied("no recovery attempts remaining")
|
||||
}
|
||||
|
||||
if err := a.issueBoundKeypairChallenge(
|
||||
ctx,
|
||||
spec.Onboarding.InitialPublicKey,
|
||||
challengeResponse,
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
if spec.Onboarding.InitialPublicKey != "" {
|
||||
// An initial public key was configured, so we can immediately ask
|
||||
// the client to complete a challenge.
|
||||
if err := a.issueBoundKeypairChallenge(
|
||||
ctx,
|
||||
spec.Onboarding.InitialPublicKey,
|
||||
challengeResponse,
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Now that we've confirmed the key, we can consider it bound.
|
||||
mutators = append(
|
||||
mutators,
|
||||
mutateStatusBoundPublicKey(spec.Onboarding.InitialPublicKey, ""),
|
||||
)
|
||||
|
||||
boundPublicKey = spec.Onboarding.InitialPublicKey
|
||||
} else if status.RegistrationSecret != "" {
|
||||
// Shared error message for all registration secret check failures.
|
||||
const errMsg = "a valid registration secret is required"
|
||||
|
||||
// A registration secret is expected.
|
||||
if req.InitialJoinSecret == "" {
|
||||
log.WarnContext(ctx, "denying join attempt, client failed to provide required registration secret")
|
||||
return nil, trace.AccessDenied(errMsg)
|
||||
}
|
||||
|
||||
if spec.Onboarding.MustRegisterBefore != nil {
|
||||
if a.clock.Now().After(*spec.Onboarding.MustRegisterBefore) {
|
||||
log.WarnContext(
|
||||
ctx,
|
||||
"denying join attempt due to expired registration secret",
|
||||
"must_register_before",
|
||||
spec.Onboarding.MustRegisterBefore,
|
||||
)
|
||||
return nil, trace.AccessDenied(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the secret.
|
||||
if subtle.ConstantTimeCompare([]byte(status.RegistrationSecret), []byte(req.InitialJoinSecret)) != 1 {
|
||||
log.WarnContext(ctx, "denying join attempt, client provided incorrect registration secret")
|
||||
return nil, trace.AccessDenied(errMsg)
|
||||
}
|
||||
|
||||
// Ask the client for a new public key.
|
||||
newPubKey, err := a.requestBoundKeypairRotation(ctx, challengeResponse)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "requesting public key")
|
||||
}
|
||||
|
||||
// The rotation process verifies private key ownership, so we can
|
||||
// consider it it bound. Note that for our purposes here, this
|
||||
// initial join will not count as a rotation.
|
||||
mutators = append(
|
||||
mutators,
|
||||
mutateStatusBoundPublicKey(newPubKey, ""),
|
||||
mutateStatusClearRegistrationSecret(status.RegistrationSecret),
|
||||
)
|
||||
|
||||
boundPublicKey = newPubKey
|
||||
} else {
|
||||
return nil, trace.BadParameter("either an initial public key or registration secret is required")
|
||||
}
|
||||
|
||||
// Now that we've confirmed the key, we can consider it bound.
|
||||
// If we reach this point, it counts as a recovery, so add a join
|
||||
// mutator.
|
||||
mutators = append(
|
||||
mutators,
|
||||
mutateStatusBoundPublicKey(spec.Onboarding.InitialPublicKey, ""),
|
||||
mutateStatusConsumeJoin(recoveryMode, status.RecoveryCount, spec.Recovery.Limit),
|
||||
mutateStatusConsumeRecovery(recoveryMode, status.RecoveryCount, spec.Recovery.Limit),
|
||||
)
|
||||
|
||||
expectNewBotInstance = true
|
||||
boundPublicKey = spec.Onboarding.InitialPublicKey
|
||||
case !hasBoundPublicKey && hasIncomingBotInstance:
|
||||
// Not allowed, at least at the moment. This would imply e.g. trying to
|
||||
// change auth methods.
|
||||
@@ -592,7 +701,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
|
||||
|
||||
mutators = append(
|
||||
mutators,
|
||||
mutateStatusConsumeJoin(recoveryMode, status.RecoveryCount, spec.Recovery.Limit),
|
||||
mutateStatusConsumeRecovery(recoveryMode, status.RecoveryCount, spec.Recovery.Limit),
|
||||
)
|
||||
|
||||
expectNewBotInstance = true
|
||||
|
||||
@@ -98,6 +98,9 @@ func parseJoinState(t *testing.T, state []byte) *boundkeypair.JoinState {
|
||||
func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// TODO: This prevents parallel execution; remove along with the experiment.
|
||||
boundkeypairexperiment.SetEnabled(true)
|
||||
|
||||
_, correctPublicKey := testBoundKeypair(t)
|
||||
_, rotatedPublicKey := testBoundKeypair(t)
|
||||
_, incorrectPublicKey := testBoundKeypair(t)
|
||||
@@ -243,7 +246,9 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
}
|
||||
|
||||
makeSolver := func(initialPubKey string, mutators ...func(s *wrappedSolver)) *wrappedSolver {
|
||||
wrapper := &wrappedSolver{}
|
||||
wrapper := &wrappedSolver{
|
||||
solutions: []string{},
|
||||
}
|
||||
for _, mutator := range mutators {
|
||||
mutator(wrapper)
|
||||
}
|
||||
@@ -708,6 +713,138 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "registration-success",
|
||||
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
||||
v2.Spec.BoundKeypair.Onboarding.RegistrationSecret = "secret"
|
||||
}),
|
||||
initReq: makeInitReq(func(r *proto.RegisterUsingBoundKeypairInitialRequest) {
|
||||
// note that we'll need to specify a secret here since there's
|
||||
// not a good way to plumb the auto-generated secret back to the
|
||||
// test.
|
||||
r.InitialJoinSecret = "secret"
|
||||
}),
|
||||
solver: makeSolver("", withRotatedPubKey(correctPublicKey)),
|
||||
|
||||
assertError: require.NoError,
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 1, s.challengeCount)
|
||||
require.EqualValues(t, 1, s.rotationCount)
|
||||
|
||||
// we'll only be asked for one challenge
|
||||
require.Equal(t, []string{correctPublicKey}, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Equal(t, correctPublicKey, res.BoundPublicKey)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "registration-failure-wrong-secret",
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
||||
v2.Spec.BoundKeypair.Onboarding.RegistrationSecret = "secret"
|
||||
}),
|
||||
initReq: makeInitReq(func(r *proto.RegisterUsingBoundKeypairInitialRequest) {
|
||||
r.InitialJoinSecret = "asdf"
|
||||
}),
|
||||
solver: makeSolver("", withRotatedPubKey(correctPublicKey)),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
require.ErrorContains(t, err, "a valid registration secret is required")
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 0, s.challengeCount)
|
||||
require.EqualValues(t, 0, s.rotationCount)
|
||||
require.Empty(t, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Empty(t, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
{
|
||||
// in this case, the server will generate a registration secret
|
||||
// automatically since nothing was set in .Onboarding. we won't know
|
||||
// it in the test, but will know it tried to check the provided
|
||||
// secret due to the error message.
|
||||
name: "registration-failure-wrong-secret-autogenerated",
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
||||
v2.Spec.BoundKeypair.Onboarding.RegistrationSecret = ""
|
||||
}),
|
||||
initReq: makeInitReq(func(r *proto.RegisterUsingBoundKeypairInitialRequest) {
|
||||
r.InitialJoinSecret = "asdf"
|
||||
}),
|
||||
solver: makeSolver(""),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
require.ErrorContains(t, err, "a valid registration secret is required")
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 0, s.challengeCount)
|
||||
require.EqualValues(t, 0, s.rotationCount)
|
||||
require.Equal(t, []string{}, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Empty(t, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Joining with the a secret when a key was expected should be
|
||||
// handled as if the client couldn't complete the challenge (which
|
||||
// it can't)
|
||||
name: "registration-failure-expected-key",
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.Onboarding.InitialPublicKey = correctPublicKey
|
||||
}),
|
||||
initReq: makeInitReq(func(r *proto.RegisterUsingBoundKeypairInitialRequest) {
|
||||
r.InitialJoinSecret = "asdf"
|
||||
}),
|
||||
solver: makeSolver("", withRotatedPubKey(rotatedPublicKey)),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
// note: error generated by our mock solver above
|
||||
require.ErrorContains(t, err, "wrong public key")
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 1, s.challengeCount)
|
||||
require.EqualValues(t, 0, s.rotationCount)
|
||||
require.Equal(t, []string{}, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Empty(t, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "registration-failure-secret-expired",
|
||||
token: makeToken(func(v2 *types.ProvisionTokenV2) {
|
||||
v2.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
||||
v2.Spec.BoundKeypair.Onboarding.RegistrationSecret = "secret"
|
||||
v2.Spec.BoundKeypair.Onboarding.MustRegisterBefore = &startTime
|
||||
}),
|
||||
initReq: makeInitReq(func(r *proto.RegisterUsingBoundKeypairInitialRequest) {
|
||||
r.InitialJoinSecret = "secret"
|
||||
}),
|
||||
solver: makeSolver("", withRotatedPubKey(rotatedPublicKey)),
|
||||
|
||||
assertError: func(tt require.TestingT, err error, i ...interface{}) {
|
||||
require.ErrorContains(t, err, "a valid registration secret is required")
|
||||
},
|
||||
assertSolverState: func(t *testing.T, s *wrappedSolver) {
|
||||
require.EqualValues(t, 0, s.challengeCount)
|
||||
require.EqualValues(t, 0, s.rotationCount)
|
||||
require.Empty(t, s.solutions)
|
||||
},
|
||||
assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
|
||||
require.Empty(t, v2.Status.BoundKeypair.BoundPublicKey)
|
||||
require.Nil(t, res)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -716,7 +853,11 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
|
||||
tt.name, time.Now().Add(2*time.Hour), tt.token.Spec, tt.token.Status,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, auth.CreateToken(ctx, token))
|
||||
|
||||
// note: we only override CreateToken in ServerWithRoles, so we'll
|
||||
// need to call CreateBoundKeypairToken() directly to ensure
|
||||
// computed fields (i.e. registration secrets) are handled properly.
|
||||
require.NoError(t, auth.CreateBoundKeypairToken(ctx, token))
|
||||
tt.initReq.JoinRequest.Token = tt.name
|
||||
|
||||
response, err := auth.RegisterUsingBoundKeypairMethod(ctx, tt.initReq, tt.solver.wrapped)
|
||||
|
||||
@@ -402,11 +402,6 @@ func (s *JoinServiceGRPCServer) registerUsingBoundKeypair(srv proto.JoinService_
|
||||
return trace.BadParameter("expected non-nil Init payload")
|
||||
}
|
||||
|
||||
if initReq.InitialJoinSecret != "" {
|
||||
// TODO: not supported yet.
|
||||
return trace.NotImplemented("initial join secrets are not yet supported")
|
||||
}
|
||||
|
||||
if initReq.JoinRequest == nil {
|
||||
return trace.BadParameter(
|
||||
"expected JoinRequest in RegisterUsingBoundKeypairInitialRequest, got nil",
|
||||
|
||||
@@ -106,14 +106,14 @@ func (a *AuthProxyArgs) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error
|
||||
type sharedStartArgs struct {
|
||||
*AuthProxyArgs
|
||||
|
||||
JoinMethod string
|
||||
Token string
|
||||
CAPins []string
|
||||
CertificateTTL time.Duration
|
||||
RenewalInterval time.Duration
|
||||
Storage string
|
||||
InitialJoinSecret string
|
||||
Keypair string
|
||||
JoinMethod string
|
||||
Token string
|
||||
CAPins []string
|
||||
CertificateTTL time.Duration
|
||||
RenewalInterval time.Duration
|
||||
Storage string
|
||||
RegistrationSecret string
|
||||
Keypair string
|
||||
|
||||
Oneshot bool
|
||||
DiagAddr string
|
||||
@@ -139,7 +139,7 @@ func newSharedStartArgs(cmd *kingpin.CmdClause) *sharedStartArgs {
|
||||
cmd.Flag("oneshot", "If set, quit after the first renewal.").IsSetByUser(&args.oneshotSetByUser).BoolVar(&args.Oneshot)
|
||||
cmd.Flag("diag-addr", "If set and the bot is in debug mode, a diagnostics service will listen on specified address.").StringVar(&args.DiagAddr)
|
||||
cmd.Flag("storage", "A destination URI for tbot's internal storage, e.g. file:///foo/bar").StringVar(&args.Storage)
|
||||
cmd.Flag("initial-join-secret", "For bound keypair joining, specifies an initial joining secret.").StringVar(&args.InitialJoinSecret)
|
||||
cmd.Flag("registration-secret", "For bound keypair joining, specifies a registration secret for use at first join.").StringVar(&args.RegistrationSecret)
|
||||
|
||||
return args
|
||||
}
|
||||
@@ -238,12 +238,12 @@ func (s *sharedStartArgs) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) err
|
||||
cfg.Onboarding.SetToken(s.Token)
|
||||
}
|
||||
|
||||
if s.JoinMethod != string(types.JoinMethodBoundKeypair) && s.InitialJoinSecret != "" {
|
||||
return trace.BadParameter("--initial-join-secret and --keypair are only valid with --join-method=%s", types.JoinMethodBoundKeypair)
|
||||
if s.JoinMethod != string(types.JoinMethodBoundKeypair) && s.RegistrationSecret != "" {
|
||||
return trace.BadParameter("--registration-secret is only valid with --join-method=%s", types.JoinMethodBoundKeypair)
|
||||
}
|
||||
|
||||
if s.InitialJoinSecret != "" {
|
||||
cfg.Onboarding.BoundKeypair.InitialJoinSecret = s.InitialJoinSecret
|
||||
if s.RegistrationSecret != "" {
|
||||
cfg.Onboarding.BoundKeypair.InitialJoinSecret = s.RegistrationSecret
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -600,8 +600,16 @@ func botIdentityFromToken(
|
||||
adapter := config.NewBoundkeypairDestinationAdapter(cfg.Storage.Destination)
|
||||
boundKeypairState, err = boundkeypair.LoadClientState(ctx, adapter)
|
||||
if trace.IsNotFound(err) && joinSecret != "" {
|
||||
return nil, trace.NotImplemented("no existing client state was found and join secrets are not yet supported")
|
||||
log.InfoContext(ctx, "No existing client state found, will attempt "+
|
||||
"to join with provided registration secret")
|
||||
boundKeypairState = boundkeypair.NewEmptyClientState(adapter)
|
||||
} else if err != nil {
|
||||
log.ErrorContext(ctx, "Could not complete bound keypair joining as "+
|
||||
"no local credentials are available and no registration secret "+
|
||||
"was provided. To continue, either generate a keypair with "+
|
||||
"`tbot keypair create` and register it with Teleport, or "+
|
||||
"generate a registration secret on Teleport and provide it with"+
|
||||
"the `--registration-secret` flag.")
|
||||
return nil, trace.Wrap(err, "loading bound keypair client state")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user