diff --git a/api/gen/proto/go/teleport/join/v1/joinservice.pb.go b/api/gen/proto/go/teleport/join/v1/joinservice.pb.go
index 93c0b0bf6d0f4..3a9ca8717d7c6 100644
--- a/api/gen/proto/go/teleport/join/v1/joinservice.pb.go
+++ b/api/gen/proto/go/teleport/join/v1/joinservice.pb.go
@@ -450,6 +450,440 @@ func (x *TokenInit) GetClientParams() *ClientParams {
return nil
}
+// BoundKeypairInit is sent from the client in response to the ServerInit
+// message for the bound keypair join method.
+// The server is expected to respond with a BoundKeypairChallenge.
+//
+// The bound keypair method join flow is:
+// 1. client->server: ClientInit
+// 2. server->client: ServerInit
+// 3. client->server: BoundKeypairInit
+// 4. server->client: BoundKeypairChallenge
+// 5. client->server: BoundKeypairChallengeSolution
+// (optional additional steps if keypair rotation is required)
+// server->client: BoundKeypairRotationRequest
+// client->server: BoundKeypairRotationResponse
+// server->client: BoundKeypairChallenge
+// client->server: BoundKeypairChallengeSolution
+// 6. server->client: Result containing BoundKeypairResult
+type BoundKeypairInit struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // ClientParams holds parameters for the specific type of client trying to join.
+ ClientParams *ClientParams `protobuf:"bytes,1,opt,name=client_params,json=clientParams,proto3" json:"client_params,omitempty"`
+ // If set, attempts to bind a new keypair using an initial join secret.
+ // Any value set here will be ignored if a keypair is already bound.
+ InitialJoinSecret string `protobuf:"bytes,2,opt,name=initial_join_secret,json=initialJoinSecret,proto3" json:"initial_join_secret,omitempty"`
+ // A document signed by Auth containing join state parameters from the
+ // previous join attempt. Not required on initial join; required on all
+ // subsequent joins.
+ PreviousJoinState []byte `protobuf:"bytes,3,opt,name=previous_join_state,json=previousJoinState,proto3" json:"previous_join_state,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoundKeypairInit) Reset() {
+ *x = BoundKeypairInit{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoundKeypairInit) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoundKeypairInit) ProtoMessage() {}
+
+func (x *BoundKeypairInit) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoundKeypairInit.ProtoReflect.Descriptor instead.
+func (*BoundKeypairInit) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *BoundKeypairInit) GetClientParams() *ClientParams {
+ if x != nil {
+ return x.ClientParams
+ }
+ return nil
+}
+
+func (x *BoundKeypairInit) GetInitialJoinSecret() string {
+ if x != nil {
+ return x.InitialJoinSecret
+ }
+ return ""
+}
+
+func (x *BoundKeypairInit) GetPreviousJoinState() []byte {
+ if x != nil {
+ return x.PreviousJoinState
+ }
+ return nil
+}
+
+// BoundKeypairChallenge is a challenge issued by the server that joining
+// clients are expected to complete.
+// The client is expected to respond with a BoundKeypairChallengeSolution.
+type BoundKeypairChallenge struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // The desired public key corresponding to the private key that should be used
+ // to sign this challenge, in SSH authorized keys format.
+ PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+ // A challenge to sign with the requested public key. During keypair rotation,
+ // a second challenge will be provided to verify the new keypair before certs
+ // are returned.
+ Challenge string `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoundKeypairChallenge) Reset() {
+ *x = BoundKeypairChallenge{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoundKeypairChallenge) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoundKeypairChallenge) ProtoMessage() {}
+
+func (x *BoundKeypairChallenge) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoundKeypairChallenge.ProtoReflect.Descriptor instead.
+func (*BoundKeypairChallenge) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *BoundKeypairChallenge) GetPublicKey() []byte {
+ if x != nil {
+ return x.PublicKey
+ }
+ return nil
+}
+
+func (x *BoundKeypairChallenge) GetChallenge() string {
+ if x != nil {
+ return x.Challenge
+ }
+ return ""
+}
+
+// BoundKeypairChallengeSolution is sent from the client in response to the
+// BoundKeypairChallenge.
+// The server is expected to respond with either a Result or a
+// BoundKeypairRotationRequest.
+type BoundKeypairChallengeSolution struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // A solution to a challenge from the server. This generated by signing the
+ // challenge as a JWT using the keypair associated with the requested public
+ // key.
+ Solution []byte `protobuf:"bytes,1,opt,name=solution,proto3" json:"solution,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoundKeypairChallengeSolution) Reset() {
+ *x = BoundKeypairChallengeSolution{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoundKeypairChallengeSolution) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoundKeypairChallengeSolution) ProtoMessage() {}
+
+func (x *BoundKeypairChallengeSolution) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoundKeypairChallengeSolution.ProtoReflect.Descriptor instead.
+func (*BoundKeypairChallengeSolution) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *BoundKeypairChallengeSolution) GetSolution() []byte {
+ if x != nil {
+ return x.Solution
+ }
+ return nil
+}
+
+// BoundKeypairRotationRequest is sent by the server in response to a
+// BoundKeypairChallenge when a keypair rotation is required. It acts like an
+// additional challenge, the client is expected to respond with a
+// BoundKeypairRotationResponse.
+type BoundKeypairRotationRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // The signature algorithm suite in use by the cluster.
+ SignatureAlgorithmSuite string `protobuf:"bytes,1,opt,name=signature_algorithm_suite,json=signatureAlgorithmSuite,proto3" json:"signature_algorithm_suite,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoundKeypairRotationRequest) Reset() {
+ *x = BoundKeypairRotationRequest{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoundKeypairRotationRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoundKeypairRotationRequest) ProtoMessage() {}
+
+func (x *BoundKeypairRotationRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoundKeypairRotationRequest.ProtoReflect.Descriptor instead.
+func (*BoundKeypairRotationRequest) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *BoundKeypairRotationRequest) GetSignatureAlgorithmSuite() string {
+ if x != nil {
+ return x.SignatureAlgorithmSuite
+ }
+ return ""
+}
+
+// BoundKeypairRotationResponse is sent by the client in response to a
+// BoundKeypairRotationRequest from the server.
+// The server is expected to respond with an additional BoundKeypairChallenge
+// for the new key.
+type BoundKeypairRotationResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // The public key to be registered with auth. Clients should expect a
+ // subsequent challenge against this public key to be sent. This is encoded in
+ // SSH authorized keys format.
+ PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoundKeypairRotationResponse) Reset() {
+ *x = BoundKeypairRotationResponse{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoundKeypairRotationResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoundKeypairRotationResponse) ProtoMessage() {}
+
+func (x *BoundKeypairRotationResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoundKeypairRotationResponse.ProtoReflect.Descriptor instead.
+func (*BoundKeypairRotationResponse) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *BoundKeypairRotationResponse) GetPublicKey() []byte {
+ if x != nil {
+ return x.PublicKey
+ }
+ return nil
+}
+
+// BoundKeypairResult holds additional result parameters relevant to the bound
+// keypair join method.
+type BoundKeypairResult struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // A signed join state document to be provided on the next join attempt.
+ JoinState []byte `protobuf:"bytes,2,opt,name=join_state,json=joinState,proto3" json:"join_state,omitempty"`
+ // The public key registered with Auth at the end of the joining ceremony.
+ // After a successful keypair rotation, this should reflect the newly
+ // registered public key. This is encoded in SSH authorized keys format.
+ PublicKey []byte `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BoundKeypairResult) Reset() {
+ *x = BoundKeypairResult{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BoundKeypairResult) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BoundKeypairResult) ProtoMessage() {}
+
+func (x *BoundKeypairResult) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BoundKeypairResult.ProtoReflect.Descriptor instead.
+func (*BoundKeypairResult) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *BoundKeypairResult) GetJoinState() []byte {
+ if x != nil {
+ return x.JoinState
+ }
+ return nil
+}
+
+func (x *BoundKeypairResult) GetPublicKey() []byte {
+ if x != nil {
+ return x.PublicKey
+ }
+ return nil
+}
+
+// ChallengeSolution holds a solution to a challenge issued by the server.
+type ChallengeSolution struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Types that are valid to be assigned to Payload:
+ //
+ // *ChallengeSolution_BoundKeypairChallengeSolution
+ // *ChallengeSolution_BoundKeypairRotationResponse
+ Payload isChallengeSolution_Payload `protobuf_oneof:"payload"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ChallengeSolution) Reset() {
+ *x = ChallengeSolution{}
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ChallengeSolution) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChallengeSolution) ProtoMessage() {}
+
+func (x *ChallengeSolution) ProtoReflect() protoreflect.Message {
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ChallengeSolution.ProtoReflect.Descriptor instead.
+func (*ChallengeSolution) Descriptor() ([]byte, []int) {
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *ChallengeSolution) GetPayload() isChallengeSolution_Payload {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+func (x *ChallengeSolution) GetBoundKeypairChallengeSolution() *BoundKeypairChallengeSolution {
+ if x != nil {
+ if x, ok := x.Payload.(*ChallengeSolution_BoundKeypairChallengeSolution); ok {
+ return x.BoundKeypairChallengeSolution
+ }
+ }
+ return nil
+}
+
+func (x *ChallengeSolution) GetBoundKeypairRotationResponse() *BoundKeypairRotationResponse {
+ if x != nil {
+ if x, ok := x.Payload.(*ChallengeSolution_BoundKeypairRotationResponse); ok {
+ return x.BoundKeypairRotationResponse
+ }
+ }
+ return nil
+}
+
+type isChallengeSolution_Payload interface {
+ isChallengeSolution_Payload()
+}
+
+type ChallengeSolution_BoundKeypairChallengeSolution struct {
+ BoundKeypairChallengeSolution *BoundKeypairChallengeSolution `protobuf:"bytes,1,opt,name=bound_keypair_challenge_solution,json=boundKeypairChallengeSolution,proto3,oneof"`
+}
+
+type ChallengeSolution_BoundKeypairRotationResponse struct {
+ BoundKeypairRotationResponse *BoundKeypairRotationResponse `protobuf:"bytes,2,opt,name=bound_keypair_rotation_response,json=boundKeypairRotationResponse,proto3,oneof"`
+}
+
+func (*ChallengeSolution_BoundKeypairChallengeSolution) isChallengeSolution_Payload() {}
+
+func (*ChallengeSolution_BoundKeypairRotationResponse) isChallengeSolution_Payload() {}
+
// JoinRequest is the message type sent from the joining client to the server.
type JoinRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -457,6 +891,8 @@ type JoinRequest struct {
//
// *JoinRequest_ClientInit
// *JoinRequest_TokenInit
+ // *JoinRequest_BoundKeypairInit
+ // *JoinRequest_Solution
Payload isJoinRequest_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -464,7 +900,7 @@ type JoinRequest struct {
func (x *JoinRequest) Reset() {
*x = JoinRequest{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -476,7 +912,7 @@ func (x *JoinRequest) String() string {
func (*JoinRequest) ProtoMessage() {}
func (x *JoinRequest) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -489,7 +925,7 @@ func (x *JoinRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinRequest.ProtoReflect.Descriptor instead.
func (*JoinRequest) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{6}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{13}
}
func (x *JoinRequest) GetPayload() isJoinRequest_Payload {
@@ -517,6 +953,24 @@ func (x *JoinRequest) GetTokenInit() *TokenInit {
return nil
}
+func (x *JoinRequest) GetBoundKeypairInit() *BoundKeypairInit {
+ if x != nil {
+ if x, ok := x.Payload.(*JoinRequest_BoundKeypairInit); ok {
+ return x.BoundKeypairInit
+ }
+ }
+ return nil
+}
+
+func (x *JoinRequest) GetSolution() *ChallengeSolution {
+ if x != nil {
+ if x, ok := x.Payload.(*JoinRequest_Solution); ok {
+ return x.Solution
+ }
+ }
+ return nil
+}
+
type isJoinRequest_Payload interface {
isJoinRequest_Payload()
}
@@ -529,10 +983,22 @@ type JoinRequest_TokenInit struct {
TokenInit *TokenInit `protobuf:"bytes,2,opt,name=token_init,json=tokenInit,proto3,oneof"`
}
+type JoinRequest_BoundKeypairInit struct {
+ BoundKeypairInit *BoundKeypairInit `protobuf:"bytes,3,opt,name=bound_keypair_init,json=boundKeypairInit,proto3,oneof"`
+}
+
+type JoinRequest_Solution struct {
+ Solution *ChallengeSolution `protobuf:"bytes,4,opt,name=solution,proto3,oneof"`
+}
+
func (*JoinRequest_ClientInit) isJoinRequest_Payload() {}
func (*JoinRequest_TokenInit) isJoinRequest_Payload() {}
+func (*JoinRequest_BoundKeypairInit) isJoinRequest_Payload() {}
+
+func (*JoinRequest_Solution) isJoinRequest_Payload() {}
+
// ServerInit is the first message sent from the server in response to the
// ClientInit message.
type ServerInit struct {
@@ -548,7 +1014,7 @@ type ServerInit struct {
func (x *ServerInit) Reset() {
*x = ServerInit{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -560,7 +1026,7 @@ func (x *ServerInit) String() string {
func (*ServerInit) ProtoMessage() {}
func (x *ServerInit) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -573,7 +1039,7 @@ func (x *ServerInit) ProtoReflect() protoreflect.Message {
// Deprecated: Use ServerInit.ProtoReflect.Descriptor instead.
func (*ServerInit) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{7}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{14}
}
func (x *ServerInit) GetJoinMethod() string {
@@ -592,14 +1058,19 @@ func (x *ServerInit) GetSignatureAlgorithmSuite() string {
// Challenge is a challenge message sent from the server that the client must solve.
type Challenge struct {
- state protoimpl.MessageState `protogen:"open.v1"`
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Types that are valid to be assigned to Payload:
+ //
+ // *Challenge_BoundKeypairChallenge
+ // *Challenge_BoundKeypairRotationRequest
+ Payload isChallenge_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Challenge) Reset() {
*x = Challenge{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -611,7 +1082,7 @@ func (x *Challenge) String() string {
func (*Challenge) ProtoMessage() {}
func (x *Challenge) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -624,9 +1095,50 @@ func (x *Challenge) ProtoReflect() protoreflect.Message {
// Deprecated: Use Challenge.ProtoReflect.Descriptor instead.
func (*Challenge) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{8}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *Challenge) GetPayload() isChallenge_Payload {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+func (x *Challenge) GetBoundKeypairChallenge() *BoundKeypairChallenge {
+ if x != nil {
+ if x, ok := x.Payload.(*Challenge_BoundKeypairChallenge); ok {
+ return x.BoundKeypairChallenge
+ }
+ }
+ return nil
+}
+
+func (x *Challenge) GetBoundKeypairRotationRequest() *BoundKeypairRotationRequest {
+ if x != nil {
+ if x, ok := x.Payload.(*Challenge_BoundKeypairRotationRequest); ok {
+ return x.BoundKeypairRotationRequest
+ }
+ }
+ return nil
+}
+
+type isChallenge_Payload interface {
+ isChallenge_Payload()
+}
+
+type Challenge_BoundKeypairChallenge struct {
+ BoundKeypairChallenge *BoundKeypairChallenge `protobuf:"bytes,1,opt,name=bound_keypair_challenge,json=boundKeypairChallenge,proto3,oneof"`
}
+type Challenge_BoundKeypairRotationRequest struct {
+ BoundKeypairRotationRequest *BoundKeypairRotationRequest `protobuf:"bytes,2,opt,name=bound_keypair_rotation_request,json=boundKeypairRotationRequest,proto3,oneof"`
+}
+
+func (*Challenge_BoundKeypairChallenge) isChallenge_Payload() {}
+
+func (*Challenge_BoundKeypairRotationRequest) isChallenge_Payload() {}
+
// Result is the final message sent from the cluster back to the client, it
// contains the result of the joining process including the assigned host ID
// and issued certificates.
@@ -643,7 +1155,7 @@ type Result struct {
func (x *Result) Reset() {
*x = Result{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -655,7 +1167,7 @@ func (x *Result) String() string {
func (*Result) ProtoMessage() {}
func (x *Result) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -668,7 +1180,7 @@ func (x *Result) ProtoReflect() protoreflect.Message {
// Deprecated: Use Result.ProtoReflect.Descriptor instead.
func (*Result) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{9}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{16}
}
func (x *Result) GetPayload() isResult_Payload {
@@ -731,7 +1243,7 @@ type Certificates struct {
func (x *Certificates) Reset() {
*x = Certificates{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -743,7 +1255,7 @@ func (x *Certificates) String() string {
func (*Certificates) ProtoMessage() {}
func (x *Certificates) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -756,7 +1268,7 @@ func (x *Certificates) ProtoReflect() protoreflect.Message {
// Deprecated: Use Certificates.ProtoReflect.Descriptor instead.
func (*Certificates) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{10}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{17}
}
func (x *Certificates) GetTlsCert() []byte {
@@ -800,7 +1312,7 @@ type HostResult struct {
func (x *HostResult) Reset() {
*x = HostResult{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -812,7 +1324,7 @@ func (x *HostResult) String() string {
func (*HostResult) ProtoMessage() {}
func (x *HostResult) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -825,7 +1337,7 @@ func (x *HostResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use HostResult.ProtoReflect.Descriptor instead.
func (*HostResult) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{11}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{18}
}
func (x *HostResult) GetCertificates() *Certificates {
@@ -846,14 +1358,16 @@ func (x *HostResult) GetHostId() string {
type BotResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Certificates holds issued certificates and cluster CAs.
- Certificates *Certificates `protobuf:"bytes,1,opt,name=certificates,proto3" json:"certificates,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ Certificates *Certificates `protobuf:"bytes,1,opt,name=certificates,proto3" json:"certificates,omitempty"`
+ // BoundKeypairResult holds extra result parameters relevant to the bound keypair join method.
+ BoundKeypairResult *BoundKeypairResult `protobuf:"bytes,2,opt,name=bound_keypair_result,json=boundKeypairResult,proto3,oneof" json:"bound_keypair_result,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *BotResult) Reset() {
*x = BotResult{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -865,7 +1379,7 @@ func (x *BotResult) String() string {
func (*BotResult) ProtoMessage() {}
func (x *BotResult) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -878,7 +1392,7 @@ func (x *BotResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use BotResult.ProtoReflect.Descriptor instead.
func (*BotResult) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{12}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{19}
}
func (x *BotResult) GetCertificates() *Certificates {
@@ -888,6 +1402,13 @@ func (x *BotResult) GetCertificates() *Certificates {
return nil
}
+func (x *BotResult) GetBoundKeypairResult() *BoundKeypairResult {
+ if x != nil {
+ return x.BoundKeypairResult
+ }
+ return nil
+}
+
// JoinResponse is the message type sent from the server to the joining client.
type JoinResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -903,7 +1424,7 @@ type JoinResponse struct {
func (x *JoinResponse) Reset() {
*x = JoinResponse{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -915,7 +1436,7 @@ func (x *JoinResponse) String() string {
func (*JoinResponse) ProtoMessage() {}
func (x *JoinResponse) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -928,7 +1449,7 @@ func (x *JoinResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinResponse.ProtoReflect.Descriptor instead.
func (*JoinResponse) Descriptor() ([]byte, []int) {
- return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{13}
+ return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{20}
}
func (x *JoinResponse) GetPayload() isJoinResponse_Payload {
@@ -1011,7 +1532,7 @@ type ClientInit_ProxySuppliedParams struct {
func (x *ClientInit_ProxySuppliedParams) Reset() {
*x = ClientInit_ProxySuppliedParams{}
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1023,7 +1544,7 @@ func (x *ClientInit_ProxySuppliedParams) String() string {
func (*ClientInit_ProxySuppliedParams) ProtoMessage() {}
func (x *ClientInit_ProxySuppliedParams) ProtoReflect() protoreflect.Message {
- mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14]
+ mi := &file_teleport_join_v1_joinservice_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1098,19 +1619,48 @@ const file_teleport_join_v1_joinservice_proto_rawDesc = "" +
"bot_params\x18\x02 \x01(\v2\x1b.teleport.join.v1.BotParamsH\x00R\tbotParamsB\t\n" +
"\apayload\"P\n" +
"\tTokenInit\x12C\n" +
- "\rclient_params\x18\x01 \x01(\v2\x1e.teleport.join.v1.ClientParamsR\fclientParams\"\x97\x01\n" +
+ "\rclient_params\x18\x01 \x01(\v2\x1e.teleport.join.v1.ClientParamsR\fclientParams\"\xb7\x01\n" +
+ "\x10BoundKeypairInit\x12C\n" +
+ "\rclient_params\x18\x01 \x01(\v2\x1e.teleport.join.v1.ClientParamsR\fclientParams\x12.\n" +
+ "\x13initial_join_secret\x18\x02 \x01(\tR\x11initialJoinSecret\x12.\n" +
+ "\x13previous_join_state\x18\x03 \x01(\fR\x11previousJoinState\"T\n" +
+ "\x15BoundKeypairChallenge\x12\x1d\n" +
+ "\n" +
+ "public_key\x18\x01 \x01(\fR\tpublicKey\x12\x1c\n" +
+ "\tchallenge\x18\x02 \x01(\tR\tchallenge\";\n" +
+ "\x1dBoundKeypairChallengeSolution\x12\x1a\n" +
+ "\bsolution\x18\x01 \x01(\fR\bsolution\"Y\n" +
+ "\x1bBoundKeypairRotationRequest\x12:\n" +
+ "\x19signature_algorithm_suite\x18\x01 \x01(\tR\x17signatureAlgorithmSuite\"=\n" +
+ "\x1cBoundKeypairRotationResponse\x12\x1d\n" +
+ "\n" +
+ "public_key\x18\x01 \x01(\fR\tpublicKey\"R\n" +
+ "\x12BoundKeypairResult\x12\x1d\n" +
+ "\n" +
+ "join_state\x18\x02 \x01(\fR\tjoinState\x12\x1d\n" +
+ "\n" +
+ "public_key\x18\x03 \x01(\fR\tpublicKey\"\x93\x02\n" +
+ "\x11ChallengeSolution\x12z\n" +
+ " bound_keypair_challenge_solution\x18\x01 \x01(\v2/.teleport.join.v1.BoundKeypairChallengeSolutionH\x00R\x1dboundKeypairChallengeSolution\x12w\n" +
+ "\x1fbound_keypair_rotation_response\x18\x02 \x01(\v2..teleport.join.v1.BoundKeypairRotationResponseH\x00R\x1cboundKeypairRotationResponseB\t\n" +
+ "\apayload\"\xae\x02\n" +
"\vJoinRequest\x12?\n" +
"\vclient_init\x18\x01 \x01(\v2\x1c.teleport.join.v1.ClientInitH\x00R\n" +
"clientInit\x12<\n" +
"\n" +
- "token_init\x18\x02 \x01(\v2\x1b.teleport.join.v1.TokenInitH\x00R\ttokenInitB\t\n" +
+ "token_init\x18\x02 \x01(\v2\x1b.teleport.join.v1.TokenInitH\x00R\ttokenInit\x12R\n" +
+ "\x12bound_keypair_init\x18\x03 \x01(\v2\".teleport.join.v1.BoundKeypairInitH\x00R\x10boundKeypairInit\x12A\n" +
+ "\bsolution\x18\x04 \x01(\v2#.teleport.join.v1.ChallengeSolutionH\x00R\bsolutionB\t\n" +
"\apayload\"i\n" +
"\n" +
"ServerInit\x12\x1f\n" +
"\vjoin_method\x18\x01 \x01(\tR\n" +
"joinMethod\x12:\n" +
- "\x19signature_algorithm_suite\x18\x02 \x01(\tR\x17signatureAlgorithmSuite\"\v\n" +
- "\tChallenge\"\x92\x01\n" +
+ "\x19signature_algorithm_suite\x18\x02 \x01(\tR\x17signatureAlgorithmSuite\"\xef\x01\n" +
+ "\tChallenge\x12a\n" +
+ "\x17bound_keypair_challenge\x18\x01 \x01(\v2'.teleport.join.v1.BoundKeypairChallengeH\x00R\x15boundKeypairChallenge\x12t\n" +
+ "\x1ebound_keypair_rotation_request\x18\x02 \x01(\v2-.teleport.join.v1.BoundKeypairRotationRequestH\x00R\x1bboundKeypairRotationRequestB\t\n" +
+ "\apayload\"\x92\x01\n" +
"\x06Result\x12?\n" +
"\vhost_result\x18\x01 \x01(\v2\x1c.teleport.join.v1.HostResultH\x00R\n" +
"hostResult\x12<\n" +
@@ -1126,9 +1676,11 @@ const file_teleport_join_v1_joinservice_proto_rawDesc = "" +
"\n" +
"HostResult\x12B\n" +
"\fcertificates\x18\x01 \x01(\v2\x1e.teleport.join.v1.CertificatesR\fcertificates\x12\x17\n" +
- "\ahost_id\x18\x02 \x01(\tR\x06hostId\"O\n" +
+ "\ahost_id\x18\x02 \x01(\tR\x06hostId\"\xc5\x01\n" +
"\tBotResult\x12B\n" +
- "\fcertificates\x18\x01 \x01(\v2\x1e.teleport.join.v1.CertificatesR\fcertificates\"\xbe\x01\n" +
+ "\fcertificates\x18\x01 \x01(\v2\x1e.teleport.join.v1.CertificatesR\fcertificates\x12[\n" +
+ "\x14bound_keypair_result\x18\x02 \x01(\v2$.teleport.join.v1.BoundKeypairResultH\x00R\x12boundKeypairResult\x88\x01\x01B\x17\n" +
+ "\x15_bound_keypair_result\"\xbe\x01\n" +
"\fJoinResponse\x122\n" +
"\x04init\x18\x01 \x01(\v2\x1c.teleport.join.v1.ServerInitH\x00R\x04init\x12;\n" +
"\tchallenge\x18\x02 \x01(\v2\x1b.teleport.join.v1.ChallengeH\x00R\tchallenge\x122\n" +
@@ -1149,7 +1701,7 @@ func file_teleport_join_v1_joinservice_proto_rawDescGZIP() []byte {
return file_teleport_join_v1_joinservice_proto_rawDescData
}
-var file_teleport_join_v1_joinservice_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
+var file_teleport_join_v1_joinservice_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
var file_teleport_join_v1_joinservice_proto_goTypes = []any{
(*ClientInit)(nil), // 0: teleport.join.v1.ClientInit
(*PublicKeys)(nil), // 1: teleport.join.v1.PublicKeys
@@ -1157,41 +1709,56 @@ var file_teleport_join_v1_joinservice_proto_goTypes = []any{
(*BotParams)(nil), // 3: teleport.join.v1.BotParams
(*ClientParams)(nil), // 4: teleport.join.v1.ClientParams
(*TokenInit)(nil), // 5: teleport.join.v1.TokenInit
- (*JoinRequest)(nil), // 6: teleport.join.v1.JoinRequest
- (*ServerInit)(nil), // 7: teleport.join.v1.ServerInit
- (*Challenge)(nil), // 8: teleport.join.v1.Challenge
- (*Result)(nil), // 9: teleport.join.v1.Result
- (*Certificates)(nil), // 10: teleport.join.v1.Certificates
- (*HostResult)(nil), // 11: teleport.join.v1.HostResult
- (*BotResult)(nil), // 12: teleport.join.v1.BotResult
- (*JoinResponse)(nil), // 13: teleport.join.v1.JoinResponse
- (*ClientInit_ProxySuppliedParams)(nil), // 14: teleport.join.v1.ClientInit.ProxySuppliedParams
- (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
+ (*BoundKeypairInit)(nil), // 6: teleport.join.v1.BoundKeypairInit
+ (*BoundKeypairChallenge)(nil), // 7: teleport.join.v1.BoundKeypairChallenge
+ (*BoundKeypairChallengeSolution)(nil), // 8: teleport.join.v1.BoundKeypairChallengeSolution
+ (*BoundKeypairRotationRequest)(nil), // 9: teleport.join.v1.BoundKeypairRotationRequest
+ (*BoundKeypairRotationResponse)(nil), // 10: teleport.join.v1.BoundKeypairRotationResponse
+ (*BoundKeypairResult)(nil), // 11: teleport.join.v1.BoundKeypairResult
+ (*ChallengeSolution)(nil), // 12: teleport.join.v1.ChallengeSolution
+ (*JoinRequest)(nil), // 13: teleport.join.v1.JoinRequest
+ (*ServerInit)(nil), // 14: teleport.join.v1.ServerInit
+ (*Challenge)(nil), // 15: teleport.join.v1.Challenge
+ (*Result)(nil), // 16: teleport.join.v1.Result
+ (*Certificates)(nil), // 17: teleport.join.v1.Certificates
+ (*HostResult)(nil), // 18: teleport.join.v1.HostResult
+ (*BotResult)(nil), // 19: teleport.join.v1.BotResult
+ (*JoinResponse)(nil), // 20: teleport.join.v1.JoinResponse
+ (*ClientInit_ProxySuppliedParams)(nil), // 21: teleport.join.v1.ClientInit.ProxySuppliedParams
+ (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp
}
var file_teleport_join_v1_joinservice_proto_depIdxs = []int32{
- 14, // 0: teleport.join.v1.ClientInit.proxy_supplied_parameters:type_name -> teleport.join.v1.ClientInit.ProxySuppliedParams
+ 21, // 0: teleport.join.v1.ClientInit.proxy_supplied_parameters:type_name -> teleport.join.v1.ClientInit.ProxySuppliedParams
1, // 1: teleport.join.v1.HostParams.public_keys:type_name -> teleport.join.v1.PublicKeys
1, // 2: teleport.join.v1.BotParams.public_keys:type_name -> teleport.join.v1.PublicKeys
- 15, // 3: teleport.join.v1.BotParams.expires:type_name -> google.protobuf.Timestamp
+ 22, // 3: teleport.join.v1.BotParams.expires:type_name -> google.protobuf.Timestamp
2, // 4: teleport.join.v1.ClientParams.host_params:type_name -> teleport.join.v1.HostParams
3, // 5: teleport.join.v1.ClientParams.bot_params:type_name -> teleport.join.v1.BotParams
4, // 6: teleport.join.v1.TokenInit.client_params:type_name -> teleport.join.v1.ClientParams
- 0, // 7: teleport.join.v1.JoinRequest.client_init:type_name -> teleport.join.v1.ClientInit
- 5, // 8: teleport.join.v1.JoinRequest.token_init:type_name -> teleport.join.v1.TokenInit
- 11, // 9: teleport.join.v1.Result.host_result:type_name -> teleport.join.v1.HostResult
- 12, // 10: teleport.join.v1.Result.bot_result:type_name -> teleport.join.v1.BotResult
- 10, // 11: teleport.join.v1.HostResult.certificates:type_name -> teleport.join.v1.Certificates
- 10, // 12: teleport.join.v1.BotResult.certificates:type_name -> teleport.join.v1.Certificates
- 7, // 13: teleport.join.v1.JoinResponse.init:type_name -> teleport.join.v1.ServerInit
- 8, // 14: teleport.join.v1.JoinResponse.challenge:type_name -> teleport.join.v1.Challenge
- 9, // 15: teleport.join.v1.JoinResponse.result:type_name -> teleport.join.v1.Result
- 6, // 16: teleport.join.v1.JoinService.Join:input_type -> teleport.join.v1.JoinRequest
- 13, // 17: teleport.join.v1.JoinService.Join:output_type -> teleport.join.v1.JoinResponse
- 17, // [17:18] is the sub-list for method output_type
- 16, // [16:17] is the sub-list for method input_type
- 16, // [16:16] is the sub-list for extension type_name
- 16, // [16:16] is the sub-list for extension extendee
- 0, // [0:16] is the sub-list for field type_name
+ 4, // 7: teleport.join.v1.BoundKeypairInit.client_params:type_name -> teleport.join.v1.ClientParams
+ 8, // 8: teleport.join.v1.ChallengeSolution.bound_keypair_challenge_solution:type_name -> teleport.join.v1.BoundKeypairChallengeSolution
+ 10, // 9: teleport.join.v1.ChallengeSolution.bound_keypair_rotation_response:type_name -> teleport.join.v1.BoundKeypairRotationResponse
+ 0, // 10: teleport.join.v1.JoinRequest.client_init:type_name -> teleport.join.v1.ClientInit
+ 5, // 11: teleport.join.v1.JoinRequest.token_init:type_name -> teleport.join.v1.TokenInit
+ 6, // 12: teleport.join.v1.JoinRequest.bound_keypair_init:type_name -> teleport.join.v1.BoundKeypairInit
+ 12, // 13: teleport.join.v1.JoinRequest.solution:type_name -> teleport.join.v1.ChallengeSolution
+ 7, // 14: teleport.join.v1.Challenge.bound_keypair_challenge:type_name -> teleport.join.v1.BoundKeypairChallenge
+ 9, // 15: teleport.join.v1.Challenge.bound_keypair_rotation_request:type_name -> teleport.join.v1.BoundKeypairRotationRequest
+ 18, // 16: teleport.join.v1.Result.host_result:type_name -> teleport.join.v1.HostResult
+ 19, // 17: teleport.join.v1.Result.bot_result:type_name -> teleport.join.v1.BotResult
+ 17, // 18: teleport.join.v1.HostResult.certificates:type_name -> teleport.join.v1.Certificates
+ 17, // 19: teleport.join.v1.BotResult.certificates:type_name -> teleport.join.v1.Certificates
+ 11, // 20: teleport.join.v1.BotResult.bound_keypair_result:type_name -> teleport.join.v1.BoundKeypairResult
+ 14, // 21: teleport.join.v1.JoinResponse.init:type_name -> teleport.join.v1.ServerInit
+ 15, // 22: teleport.join.v1.JoinResponse.challenge:type_name -> teleport.join.v1.Challenge
+ 16, // 23: teleport.join.v1.JoinResponse.result:type_name -> teleport.join.v1.Result
+ 13, // 24: teleport.join.v1.JoinService.Join:input_type -> teleport.join.v1.JoinRequest
+ 20, // 25: teleport.join.v1.JoinService.Join:output_type -> teleport.join.v1.JoinResponse
+ 25, // [25:26] is the sub-list for method output_type
+ 24, // [24:25] is the sub-list for method input_type
+ 24, // [24:24] is the sub-list for extension type_name
+ 24, // [24:24] is the sub-list for extension extendee
+ 0, // [0:24] is the sub-list for field type_name
}
func init() { file_teleport_join_v1_joinservice_proto_init() }
@@ -1205,15 +1772,26 @@ func file_teleport_join_v1_joinservice_proto_init() {
(*ClientParams_HostParams)(nil),
(*ClientParams_BotParams)(nil),
}
- file_teleport_join_v1_joinservice_proto_msgTypes[6].OneofWrappers = []any{
+ file_teleport_join_v1_joinservice_proto_msgTypes[12].OneofWrappers = []any{
+ (*ChallengeSolution_BoundKeypairChallengeSolution)(nil),
+ (*ChallengeSolution_BoundKeypairRotationResponse)(nil),
+ }
+ file_teleport_join_v1_joinservice_proto_msgTypes[13].OneofWrappers = []any{
(*JoinRequest_ClientInit)(nil),
(*JoinRequest_TokenInit)(nil),
+ (*JoinRequest_BoundKeypairInit)(nil),
+ (*JoinRequest_Solution)(nil),
}
- file_teleport_join_v1_joinservice_proto_msgTypes[9].OneofWrappers = []any{
+ file_teleport_join_v1_joinservice_proto_msgTypes[15].OneofWrappers = []any{
+ (*Challenge_BoundKeypairChallenge)(nil),
+ (*Challenge_BoundKeypairRotationRequest)(nil),
+ }
+ file_teleport_join_v1_joinservice_proto_msgTypes[16].OneofWrappers = []any{
(*Result_HostResult)(nil),
(*Result_BotResult)(nil),
}
- file_teleport_join_v1_joinservice_proto_msgTypes[13].OneofWrappers = []any{
+ file_teleport_join_v1_joinservice_proto_msgTypes[19].OneofWrappers = []any{}
+ file_teleport_join_v1_joinservice_proto_msgTypes[20].OneofWrappers = []any{
(*JoinResponse_Init)(nil),
(*JoinResponse_Challenge)(nil),
(*JoinResponse_Result)(nil),
@@ -1224,7 +1802,7 @@ func file_teleport_join_v1_joinservice_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_join_v1_joinservice_proto_rawDesc), len(file_teleport_join_v1_joinservice_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 15,
+ NumMessages: 22,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/api/proto/teleport/join/v1/joinservice.proto b/api/proto/teleport/join/v1/joinservice.proto
index 5250c0315d403..cf9a6aa7cec28 100644
--- a/api/proto/teleport/join/v1/joinservice.proto
+++ b/api/proto/teleport/join/v1/joinservice.proto
@@ -107,11 +107,104 @@ message TokenInit {
ClientParams client_params = 1;
}
+// BoundKeypairInit is sent from the client in response to the ServerInit
+// message for the bound keypair join method.
+// The server is expected to respond with a BoundKeypairChallenge.
+//
+// The bound keypair method join flow is:
+// 1. client->server: ClientInit
+// 2. server->client: ServerInit
+// 3. client->server: BoundKeypairInit
+// 4. server->client: BoundKeypairChallenge
+// 5. client->server: BoundKeypairChallengeSolution
+// (optional additional steps if keypair rotation is required)
+// server->client: BoundKeypairRotationRequest
+// client->server: BoundKeypairRotationResponse
+// server->client: BoundKeypairChallenge
+// client->server: BoundKeypairChallengeSolution
+// 6. server->client: Result containing BoundKeypairResult
+message BoundKeypairInit {
+ // ClientParams holds parameters for the specific type of client trying to join.
+ ClientParams client_params = 1;
+ // If set, attempts to bind a new keypair using an initial join secret.
+ // Any value set here will be ignored if a keypair is already bound.
+ string initial_join_secret = 2;
+ // A document signed by Auth containing join state parameters from the
+ // previous join attempt. Not required on initial join; required on all
+ // subsequent joins.
+ bytes previous_join_state = 3;
+}
+
+// BoundKeypairChallenge is a challenge issued by the server that joining
+// clients are expected to complete.
+// The client is expected to respond with a BoundKeypairChallengeSolution.
+message BoundKeypairChallenge {
+ // The desired public key corresponding to the private key that should be used
+ // to sign this challenge, in SSH authorized keys format.
+ bytes public_key = 1;
+ // A challenge to sign with the requested public key. During keypair rotation,
+ // a second challenge will be provided to verify the new keypair before certs
+ // are returned.
+ string challenge = 2;
+}
+
+// BoundKeypairChallengeSolution is sent from the client in response to the
+// BoundKeypairChallenge.
+// The server is expected to respond with either a Result or a
+// BoundKeypairRotationRequest.
+message BoundKeypairChallengeSolution {
+ // A solution to a challenge from the server. This generated by signing the
+ // challenge as a JWT using the keypair associated with the requested public
+ // key.
+ bytes solution = 1;
+}
+
+// BoundKeypairRotationRequest is sent by the server in response to a
+// BoundKeypairChallenge when a keypair rotation is required. It acts like an
+// additional challenge, the client is expected to respond with a
+// BoundKeypairRotationResponse.
+message BoundKeypairRotationRequest {
+ // The signature algorithm suite in use by the cluster.
+ string signature_algorithm_suite = 1;
+}
+
+// BoundKeypairRotationResponse is sent by the client in response to a
+// BoundKeypairRotationRequest from the server.
+// The server is expected to respond with an additional BoundKeypairChallenge
+// for the new key.
+message BoundKeypairRotationResponse {
+ // The public key to be registered with auth. Clients should expect a
+ // subsequent challenge against this public key to be sent. This is encoded in
+ // SSH authorized keys format.
+ bytes public_key = 1;
+}
+
+// BoundKeypairResult holds additional result parameters relevant to the bound
+// keypair join method.
+message BoundKeypairResult {
+ // A signed join state document to be provided on the next join attempt.
+ bytes join_state = 2;
+ // The public key registered with Auth at the end of the joining ceremony.
+ // After a successful keypair rotation, this should reflect the newly
+ // registered public key. This is encoded in SSH authorized keys format.
+ bytes public_key = 3;
+}
+
+// ChallengeSolution holds a solution to a challenge issued by the server.
+message ChallengeSolution {
+ oneof payload {
+ BoundKeypairChallengeSolution bound_keypair_challenge_solution = 1;
+ BoundKeypairRotationResponse bound_keypair_rotation_response = 2;
+ }
+}
+
// JoinRequest is the message type sent from the joining client to the server.
message JoinRequest {
oneof payload {
ClientInit client_init = 1;
TokenInit token_init = 2;
+ BoundKeypairInit bound_keypair_init = 3;
+ ChallengeSolution solution = 4;
}
}
@@ -126,7 +219,12 @@ message ServerInit {
}
// Challenge is a challenge message sent from the server that the client must solve.
-message Challenge {}
+message Challenge {
+ oneof payload {
+ BoundKeypairChallenge bound_keypair_challenge = 1;
+ BoundKeypairRotationRequest bound_keypair_rotation_request = 2;
+ }
+}
// Result is the final message sent from the cluster back to the client, it
// contains the result of the joining process including the assigned host ID
@@ -164,6 +262,8 @@ message HostResult {
message BotResult {
// Certificates holds issued certificates and cluster CAs.
Certificates certificates = 1;
+ // BoundKeypairResult holds extra result parameters relevant to the bound keypair join method.
+ optional BoundKeypairResult bound_keypair_result = 2;
}
// JoinResponse is the message type sent from the server to the joining client.
diff --git a/lib/auth/auth.go b/lib/auth/auth.go
index 8b701efcdeb1e..f48dd1e710ad2 100644
--- a/lib/auth/auth.go
+++ b/lib/auth/auth.go
@@ -118,6 +118,7 @@ import (
"github.com/gravitational/teleport/lib/integrations/awsra/createsession"
"github.com/gravitational/teleport/lib/inventory"
iterstream "github.com/gravitational/teleport/lib/itertools/stream"
+ joinboundkeypair "github.com/gravitational/teleport/lib/join/boundkeypair"
kubetoken "github.com/gravitational/teleport/lib/kube/token"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/loginrule"
@@ -780,7 +781,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (as *Server, err error) {
}
if as.createBoundKeypairValidator == nil {
- as.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) {
+ as.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) {
return boundkeypair.NewChallengeValidator(subject, clusterName, publicKey)
}
}
@@ -1267,7 +1268,7 @@ type Server struct {
// createBoundKeypairValidator is a helper to create new bound keypair
// challenge validators. Used to override the implementation used in tests.
- createBoundKeypairValidator createBoundKeypairValidator
+ createBoundKeypairValidator joinboundkeypair.CreateBoundKeypairValidator
// loadAllCAs tells tsh to load the host CAs for all clusters when trying to ssh into a node.
loadAllCAs bool
@@ -1492,6 +1493,12 @@ func (a *Server) SetLockWatcher(lockWatcher *services.LockWatcher) {
a.lockWatcher = lockWatcher
}
+// CheckLockInForce returns an AccessDenied error if there is a lock in force
+// matching at least one of the targets.
+func (a *Server) CheckLockInForce(mode constants.LockingMode, targets []types.LockTarget) error {
+ return a.checkLockInForce(mode, targets)
+}
+
func (a *Server) checkLockInForce(mode constants.LockingMode, targets []types.LockTarget) error {
a.lock.RLock()
defer a.lock.RUnlock()
diff --git a/lib/auth/bound_keypair_tokens.go b/lib/auth/bound_keypair_tokens.go
new file mode 100644
index 0000000000000..82af4823345e5
--- /dev/null
+++ b/lib/auth/bound_keypair_tokens.go
@@ -0,0 +1,148 @@
+// Teleport
+// Copyright (C) 2025 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 .
+
+package auth
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/defaults"
+ "github.com/gravitational/teleport/lib/utils"
+)
+
+// validateBoundKeypairTokenSpec performs some basic validation checks on a
+// bound_keypair-type join token.
+func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair) error {
+ if spec.Recovery == nil {
+ 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{}
+ }
+ if v2.Spec.BoundKeypair.Onboarding == nil {
+ v2.Spec.BoundKeypair.Onboarding = &types.ProvisionTokenSpecV2BoundKeypair_OnboardingSpec{}
+ }
+
+ 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")
+ }
+
+ tokenV2, ok := token.(*types.ProvisionTokenV2)
+ if !ok {
+ return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle)
+ }
+
+ spec := tokenV2.Spec.BoundKeypair
+ if spec == nil {
+ return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair")
+ }
+
+ if err := validateBoundKeypairTokenSpec(spec); err != nil {
+ return trace.Wrap(err)
+ }
+
+ // Not as much to do here - ideally we'd like to prevent users from
+ // tampering with the status field, but we don't have a good mechanism to
+ // 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.
+
+ if err := populateRegistrationSecret(tokenV2); err != nil {
+ return trace.Wrap(err)
+ }
+
+ return trace.Wrap(a.CreateToken(ctx, tokenV2))
+}
+
+func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error {
+ if token.GetJoinMethod() != types.JoinMethodBoundKeypair {
+ return trace.BadParameter("must be called with a bound keypair token")
+ }
+
+ tokenV2, ok := token.(*types.ProvisionTokenV2)
+ if !ok {
+ return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle)
+ }
+
+ spec := tokenV2.Spec.BoundKeypair
+ if spec == nil {
+ return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair")
+ }
+
+ if err := validateBoundKeypairTokenSpec(spec); err != nil {
+ return trace.Wrap(err)
+ }
+
+ 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()`
+ return trace.Wrap(a.UpsertToken(ctx, token))
+}
diff --git a/lib/auth/export_test.go b/lib/auth/export_test.go
index d962ec391aa96..f11dd8278f066 100644
--- a/lib/auth/export_test.go
+++ b/lib/auth/export_test.go
@@ -18,7 +18,6 @@ package auth
import (
"context"
- "crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
@@ -46,6 +45,7 @@ import (
"github.com/gravitational/teleport/lib/circleci"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/inventory"
+ "github.com/gravitational/teleport/lib/join/boundkeypair"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tpm"
"github.com/gravitational/teleport/lib/utils"
@@ -249,14 +249,8 @@ func (a *Server) SetGHAIDTokenJWKSValidator(validator ghaIDTokenJWKSValidator) {
a.ghaIDTokenJWKSValidator = validator
}
-type BoundKeypairValidator = boundKeypairValidator
-
-type CreateBoundKeypairValidator func(subject string, clusterName string, publicKey crypto.PublicKey) (BoundKeypairValidator, error)
-
-func (a *Server) SetCreateBoundKeypairValidator(validator CreateBoundKeypairValidator) {
- a.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) {
- return validator(subject, clusterName, publicKey)
- }
+func (a *Server) SetCreateBoundKeypairValidator(validator boundkeypair.CreateBoundKeypairValidator) {
+ a.createBoundKeypairValidator = validator
}
func (a *Server) AuthenticateUserLogin(ctx context.Context, req authclient.AuthenticateUserRequest) (services.UserState, *services.SplitAccessChecker, error) {
diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go
index de42805c8484c..4828ccd3bb6d6 100644
--- a/lib/auth/grpcserver.go
+++ b/lib/auth/grpcserver.go
@@ -5850,6 +5850,7 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
joinv1.RegisterJoinServiceServer(server, join.NewServer(&join.ServerConfig{
Authorizer: cfg.Authorizer,
AuthService: cfg.AuthServer,
+ Clock: cfg.AuthServer.clock,
}))
integrationServiceServer, err := integrationv1.NewService(&integrationv1.ServiceConfig{
diff --git a/lib/auth/join_bound_keypair_adapter.go b/lib/auth/join_bound_keypair_adapter.go
new file mode 100644
index 0000000000000..dce5da8b59082
--- /dev/null
+++ b/lib/auth/join_bound_keypair_adapter.go
@@ -0,0 +1,38 @@
+// Teleport
+// Copyright (C) 2025 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 .
+
+package auth
+
+import (
+ "context"
+
+ "github.com/gravitational/teleport/api/client"
+ "github.com/gravitational/teleport/api/client/proto"
+ "github.com/gravitational/teleport/lib/join"
+)
+
+// RegisterUsingBoundKeypairMethod handles bound keypair joining for the legacy
+// join service and accepts the legacy protobuf message types. It calls into
+// the common logic implemented in lib/join.
+//
+// TODO(nklaassen): DELETE IN 20 when removing the legacy join service.
+func (a *Server) RegisterUsingBoundKeypairMethod(
+ ctx context.Context,
+ req *proto.RegisterUsingBoundKeypairInitialRequest,
+ challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
+) (*client.BoundKeypairRegistrationResponse, error) {
+ return join.AdaptRegisterUsingBoundKeypairMethod(ctx, a, a.createBoundKeypairValidator, req, challengeResponse)
+}
diff --git a/lib/auth/join_bound_keypair_test.go b/lib/auth/join_bound_keypair_test.go
index 0c2270ea5c4c5..551ef59d8e367 100644
--- a/lib/auth/join_bound_keypair_test.go
+++ b/lib/auth/join_bound_keypair_test.go
@@ -37,11 +37,11 @@ import (
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"
- "github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/boundkeypair"
"github.com/gravitational/teleport/lib/cryptosuites"
+ joinboundkeypair "github.com/gravitational/teleport/lib/join/boundkeypair"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
)
@@ -113,7 +113,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
srv := newTestTLSServer(t, withClock(clock))
authServer := srv.Auth()
- authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) {
+ authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) {
return &mockBoundKeypairValidator{
subject: subject,
clusterName: clusterName,
@@ -960,7 +960,7 @@ func TestServer_RegisterUsingBoundKeypairMethod_GenerationCounter(t *testing.T)
srv := newTestTLSServer(t, withClock(clock))
authServer := srv.Auth()
- authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) {
+ authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) {
return &mockBoundKeypairValidator{
subject: subject,
clusterName: clusterName,
@@ -1167,7 +1167,7 @@ func TestServer_RegisterUsingBoundKeypairMethod_JoinStateFailure(t *testing.T) {
srv := newTestTLSServer(t, withClock(clock))
authServer := srv.Auth()
- authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) {
+ authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) {
return &mockBoundKeypairValidator{
subject: subject,
clusterName: clusterName,
@@ -1323,7 +1323,7 @@ func TestServer_RegisterUsingBoundKeypairMethod_JoinStateFailureDuringRenewal(t
srv := newTestTLSServer(t, withClock(clock))
authServer := srv.Auth()
- authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) {
+ authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) {
return &mockBoundKeypairValidator{
subject: subject,
clusterName: clusterName,
diff --git a/lib/join/adapters.go b/lib/join/adapters.go
new file mode 100644
index 0000000000000..4c4bcf1c9cfc4
--- /dev/null
+++ b/lib/join/adapters.go
@@ -0,0 +1,118 @@
+// Teleport
+// Copyright (C) 2025 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 .
+
+package join
+
+import (
+ "encoding/pem"
+
+ "github.com/gravitational/trace"
+ "golang.org/x/crypto/ssh"
+
+ "github.com/gravitational/teleport/api/client/proto"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/join/internal/messages"
+)
+
+func clientInitFromRegisterUsingTokenRequest(req *types.RegisterUsingTokenRequest, joinMethod string) *messages.ClientInit {
+ return &messages.ClientInit{
+ JoinMethod: &joinMethod,
+ TokenName: req.Token,
+ SystemRole: req.Role.String(),
+ }
+}
+
+func clientParamsFromRegisterUsingTokenRequest(req *types.RegisterUsingTokenRequest) (*messages.ClientParams, error) {
+ rawTLSPub, _ := pem.Decode(req.PublicTLSKey)
+ if rawTLSPub == nil {
+ return nil, trace.BadParameter("failed to decode PublicTLSKey from PEM")
+ }
+ sshPub, _, _, _, err := ssh.ParseAuthorizedKey(req.PublicSSHKey)
+ if err != nil {
+ return nil, trace.Wrap(err, "parsing PublicSSHKey from authorized_keys format")
+ }
+ publicKeys := messages.PublicKeys{
+ PublicTLSKey: rawTLSPub.Bytes,
+ PublicSSHKey: sshPub.Marshal(),
+ }
+ var clientParams messages.ClientParams
+ if req.Role == types.RoleBot {
+ clientParams.BotParams = &messages.BotParams{
+ PublicKeys: publicKeys,
+ Expires: req.Expires,
+ }
+ } else {
+ clientParams.HostParams = &messages.HostParams{
+ PublicKeys: publicKeys,
+ HostName: req.NodeName,
+ AdditionalPrincipals: req.AdditionalPrincipals,
+ DNSNames: req.DNSNames,
+ }
+ }
+ return &clientParams, nil
+}
+
+func protoCertsFromCertificates(certs messages.Certificates) (*proto.Certs, error) {
+ sshCert, err := sshPubWireFormatToAuthorizedKey(certs.SSHCert)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ sshCAKeys, err := sshPubWireFormatsToAuthorizedKeys(certs.SSHCAKeys)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &proto.Certs{
+ TLS: pemEncodeTLSCert(certs.TLSCert),
+ TLSCACerts: pemEncodeTLSCerts(certs.TLSCACerts),
+ SSH: sshCert,
+ SSHCACerts: sshCAKeys, // SSHCACerts is a misnomer, they're just public keys.
+ }, nil
+}
+
+func pemEncodeTLSCerts(rawCerts [][]byte) [][]byte {
+ out := make([][]byte, len(rawCerts))
+ for i, rawCert := range rawCerts {
+ out[i] = pemEncodeTLSCert(rawCert)
+ }
+ return out
+}
+
+func pemEncodeTLSCert(rawCert []byte) []byte {
+ return pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: rawCert,
+ })
+}
+
+func sshPubWireFormatsToAuthorizedKeys(wireFormats [][]byte) ([][]byte, error) {
+ out := make([][]byte, len(wireFormats))
+ for i, wireFormat := range wireFormats {
+ var err error
+ out[i], err = sshPubWireFormatToAuthorizedKey(wireFormat)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ }
+ return out, nil
+}
+
+func sshPubWireFormatToAuthorizedKey(wireFormat []byte) ([]byte, error) {
+ pub, err := ssh.ParsePublicKey(wireFormat)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return ssh.MarshalAuthorizedKey(pub), nil
+}
diff --git a/lib/auth/join_bound_keypair.go b/lib/join/boundkeypair/boundkeypair.go
similarity index 66%
rename from lib/auth/join_bound_keypair.go
rename to lib/join/boundkeypair/boundkeypair.go
index 9a168bacd3f04..dd9c8527429a3 100644
--- a/lib/auth/join_bound_keypair.go
+++ b/lib/join/boundkeypair/boundkeypair.go
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package auth
+package boundkeypair
import (
"context"
@@ -29,155 +29,37 @@ import (
"github.com/google/uuid"
"github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
- "github.com/gravitational/teleport/api/client"
- "github.com/gravitational/teleport/api/client/proto"
+ "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/auth/keystore"
"github.com/gravitational/teleport/lib/boundkeypair"
- "github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
+ "github.com/gravitational/teleport/lib/join/internal/authz"
+ "github.com/gravitational/teleport/lib/join/internal/diagnostic"
+ "github.com/gravitational/teleport/lib/join/internal/messages"
"github.com/gravitational/teleport/lib/jwt"
+ "github.com/gravitational/teleport/lib/services/readonly"
libsshutils "github.com/gravitational/teleport/lib/sshutils"
- "github.com/gravitational/teleport/lib/utils"
)
-type boundKeypairValidator interface {
+type BoundKeypairValidator interface {
IssueChallenge() (*boundkeypair.ChallengeDocument, error)
ValidateChallengeResponse(issued *boundkeypair.ChallengeDocument, compactResponse string) error
}
-type createBoundKeypairValidator func(subject string, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error)
-
-// validateBoundKeypairTokenSpec performs some basic validation checks on a
-// bound_keypair-type join token.
-func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair) error {
- if spec.Recovery == nil {
- 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.Spec.BoundKeypair.Onboarding == nil {
- v2.Spec.BoundKeypair.Onboarding = &types.ProvisionTokenSpecV2BoundKeypair_OnboardingSpec{}
- }
-
- 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")
- }
-
- tokenV2, ok := token.(*types.ProvisionTokenV2)
- if !ok {
- return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle)
- }
-
- spec := tokenV2.Spec.BoundKeypair
- if spec == nil {
- return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair")
- }
-
- if err := validateBoundKeypairTokenSpec(spec); err != nil {
- return trace.Wrap(err)
- }
-
- // Not as much to do here - ideally we'd like to prevent users from
- // tampering with the status field, but we don't have a good mechanism to
- // 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.
-
- if err := populateRegistrationSecret(tokenV2); err != nil {
- return trace.Wrap(err)
- }
-
- return trace.Wrap(a.CreateToken(ctx, tokenV2))
-}
-
-func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error {
- if token.GetJoinMethod() != types.JoinMethodBoundKeypair {
- return trace.BadParameter("must be called with a bound keypair token")
- }
-
- tokenV2, ok := token.(*types.ProvisionTokenV2)
- if !ok {
- return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle)
- }
-
- spec := tokenV2.Spec.BoundKeypair
- if spec == nil {
- return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair")
- }
-
- if err := validateBoundKeypairTokenSpec(spec); err != nil {
- return trace.Wrap(err)
- }
-
- 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()`
- return trace.Wrap(a.UpsertToken(ctx, token))
-}
+type CreateBoundKeypairValidator func(subject string, clusterName string, publicKey crypto.PublicKey) (BoundKeypairValidator, error)
// issueBoundKeypairChallenge creates a new challenge for the given marshaled
// public key in ssh authorized_keys format, requests a solution from the
// client using the given `challengeResponse` function, and validates the
// response.
-func (a *Server) issueBoundKeypairChallenge(
+func issueBoundKeypairChallenge(
ctx context.Context,
+ params *JoinParams,
marshaledKey string,
- challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
) error {
key, err := libsshutils.CryptoPublicKey([]byte(marshaledKey))
if err != nil {
@@ -193,14 +75,14 @@ func (a *Server) issueBoundKeypairChallenge(
return trace.Wrap(err, "determining the key ID")
}
- clusterName, err := a.GetClusterName(ctx)
+ clusterName, err := params.AuthService.GetClusterName(ctx)
if err != nil {
return trace.Wrap(err)
}
- a.logger.DebugContext(ctx, "issuing bound keypair challenge", "key_id", keyID)
+ params.Logger.DebugContext(ctx, "issuing bound keypair challenge", "key_id", keyID)
- validator, err := a.createBoundKeypairValidator(keyID, clusterName.GetClusterName(), key)
+ validator, err := params.CreateBoundKeypairValidator(keyID, clusterName.GetClusterName(), key)
if err != nil {
return trace.Wrap(err)
}
@@ -215,32 +97,23 @@ func (a *Server) issueBoundKeypairChallenge(
return trace.Wrap(err)
}
- response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{
- Response: &proto.RegisterUsingBoundKeypairMethodResponse_Challenge{
- Challenge: &proto.RegisterUsingBoundKeypairChallenge{
- PublicKey: marshaledKey,
- Challenge: string(marshalledChallenge),
- },
- },
+ solution, err := params.IssueChallenge(&messages.BoundKeypairChallenge{
+ PublicKey: []byte(marshaledKey),
+ Challenge: string(marshalledChallenge),
})
if err != nil {
return trace.Wrap(err, "requesting a signed challenge")
}
- solutionResponse, ok := response.Payload.(*proto.RegisterUsingBoundKeypairMethodRequest_ChallengeResponse)
- if !ok {
- return trace.BadParameter("client provided unexpected challenge response type %T", response.Payload)
- }
-
if err := validator.ValidateChallengeResponse(
challenge,
- string(solutionResponse.ChallengeResponse.Solution),
+ string(solution.Solution),
); err != nil {
// TODO: Consider access denied instead?
return trace.Wrap(err, "validating challenge response")
}
- a.logger.InfoContext(ctx, "bound keypair challenge response verified successfully", "key_id", keyID)
+ params.Logger.InfoContext(ctx, "bound keypair challenge response verified successfully", "key_id", keyID)
return nil
}
@@ -300,37 +173,29 @@ func ensurePublicKeysNotEqual(a, b string) error {
// requestBoundKeypairRotation requests that clients generate a new keypair and
// send the public key, then issues a signing challenge to ensure ownership of
// the new key.
-func (a *Server) requestBoundKeypairRotation(
+func requestBoundKeypairRotation(
ctx context.Context,
- challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
+ params *JoinParams,
) (string, error) {
- cap, err := a.GetAuthPreference(ctx)
+ cap, err := params.AuthService.GetAuthPreference(ctx)
if err != nil {
return "", trace.Wrap(err)
}
- a.logger.InfoContext(ctx, "requesting bound keypair rotation", "suite", cap.GetSignatureAlgorithmSuite())
+ params.Logger.InfoContext(ctx, "requesting bound keypair rotation", "suite", cap.GetSignatureAlgorithmSuite())
// Request a new marshaled public key from the client.
- response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{
- Response: &proto.RegisterUsingBoundKeypairMethodResponse_Rotation{
- Rotation: &proto.RegisterUsingBoundKeypairRotationRequest{
- SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(),
- },
- },
+ algoSuite := types.SignatureAlgorithmSuiteToString(cap.GetSignatureAlgorithmSuite())
+ rotationResponse, err := params.IssueRotationRequest(&messages.BoundKeypairRotationRequest{
+ SignatureAlgorithmSuite: algoSuite,
})
if err != nil {
return "", trace.Wrap(err, "requesting a new public key")
}
- pubKeyResponse, ok := response.Payload.(*proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse)
- if !ok {
- return "", trace.BadParameter("client provided unexpected keypair request response type %T", response.Payload)
- }
-
// Issue a challenge against this new key to ensure ownership.
- pubKey := pubKeyResponse.RotationResponse.PublicKey
- if err := a.issueBoundKeypairChallenge(ctx, pubKey, challengeResponse); err != nil {
+ pubKey := string(rotationResponse.PublicKey)
+ if err := issueBoundKeypairChallenge(ctx, params, pubKey); err != nil {
return "", trace.Wrap(err, "solving challenge for new public key")
}
@@ -448,9 +313,9 @@ func formatTimePointer(t *time.Time) string {
// emitBoundKeypairRecoveryEvent emits an audit event indicating a bound keypair
// token was used to recover a bot.
-func (a *Server) emitBoundKeypairRecoveryEvent(
+func emitBoundKeypairRecoveryEvent(
ctx context.Context,
- req *proto.RegisterUsingBoundKeypairInitialRequest,
+ params *JoinParams,
token *types.ProvisionTokenV2,
boundPublicKey string,
recoveryCount uint32,
@@ -468,14 +333,14 @@ func (a *Server) emitBoundKeypairRecoveryEvent(
}
}
- if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.BoundKeypairRecovery{
+ if err := params.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), &apievents.BoundKeypairRecovery{
Metadata: apievents.Metadata{
Type: events.BoundKeypairRecovery,
Code: events.BoundKeypairRecoveryCode,
},
Status: status,
ConnectionMetadata: apievents.ConnectionMetadata{
- RemoteAddr: req.JoinRequest.RemoteAddr,
+ RemoteAddr: params.Diag.Get().RemoteAddr,
},
TokenName: token.GetName(),
BotName: token.GetBotName(),
@@ -483,15 +348,15 @@ func (a *Server) emitBoundKeypairRecoveryEvent(
RecoveryCount: recoveryCount,
RecoveryMode: token.Spec.BoundKeypair.Recovery.Mode,
}); err != nil {
- a.logger.WarnContext(ctx, "Failed to emit failed bound keypair recovery event", "error", err)
+ params.Logger.WarnContext(ctx, "Failed to emit failed bound keypair recovery event", "error", err)
}
}
// emitBoundKeypairRotationEvent emits an audit event indicating a bound keypair
// rotation occurred.
-func (a *Server) emitBoundKeypairRotationEvent(
+func emitBoundKeypairRotationEvent(
ctx context.Context,
- req *proto.RegisterUsingBoundKeypairInitialRequest,
+ params *JoinParams,
token *types.ProvisionTokenV2,
prevPublicKey, newPublicKey string,
err error,
@@ -508,33 +373,33 @@ func (a *Server) emitBoundKeypairRotationEvent(
}
}
- if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.BoundKeypairRotation{
+ if err := params.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), &apievents.BoundKeypairRotation{
Metadata: apievents.Metadata{
Type: events.BoundKeypairRotation,
Code: events.BoundKeypairRotationCode,
},
Status: status,
ConnectionMetadata: apievents.ConnectionMetadata{
- RemoteAddr: req.JoinRequest.RemoteAddr,
+ RemoteAddr: params.Diag.Get().RemoteAddr,
},
TokenName: token.GetName(),
BotName: token.GetBotName(),
PreviousPublicKey: prevPublicKey,
NewPublicKey: newPublicKey,
}); err != nil {
- a.logger.WarnContext(ctx, "Failed to emit failed bound keypair rotation event", "error", err)
+ params.Logger.WarnContext(ctx, "Failed to emit failed bound keypair rotation event", "error", err)
}
}
-func (a *Server) tryLockBotInvalidJoinState(
+func tryLockBotInvalidJoinState(
ctx context.Context,
+ params *JoinParams,
ptv2 *types.ProvisionTokenV2,
- req *proto.RegisterUsingBoundKeypairInitialRequest,
validationError error,
) {
- log := a.logger.With("join_token", ptv2.GetName(), "validation_error", validationError)
+ log := params.Logger.With("join_token", ptv2.GetName(), "validation_error", validationError)
- if auditErr := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.BoundKeypairJoinStateVerificationFailed{
+ if auditErr := params.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), &apievents.BoundKeypairJoinStateVerificationFailed{
Metadata: apievents.Metadata{
Type: events.BoundKeypairJoinStateVerificationFailed,
Code: events.BoundKeypairJoinStateVerificationFailedCode,
@@ -544,7 +409,7 @@ func (a *Server) tryLockBotInvalidJoinState(
Error: validationError.Error(),
},
ConnectionMetadata: apievents.ConnectionMetadata{
- RemoteAddr: req.JoinRequest.RemoteAddr,
+ RemoteAddr: params.Diag.Get().RemoteAddr,
},
TokenName: ptv2.GetName(),
BotName: ptv2.GetBotName(),
@@ -563,13 +428,13 @@ func (a *Server) tryLockBotInvalidJoinState(
"stolen keypair.",
ptv2.GetName(), ptv2.GetBotName(),
),
- CreatedAt: a.clock.Now(),
+ CreatedAt: params.Clock.Now(),
})
if err != nil {
- a.logger.ErrorContext(ctx, "Unable to create lock for bound keypair token")
+ params.Logger.ErrorContext(ctx, "Unable to create lock for bound keypair token")
return
}
- if err := a.UpsertLock(ctx, lock); err != nil {
+ if err := params.AuthService.UpsertLock(ctx, lock); err != nil {
log.ErrorContext(ctx, "Unable to create lock for bound keypair token after join state verification failed")
}
}
@@ -581,16 +446,15 @@ func (a *Server) tryLockBotInvalidJoinState(
// compromised. If verification is not required, this is a no-op. Join state
// should be verified whenever a client rejoins, but only after they have proven
// ownership of their private key.
-func (a *Server) verifyBoundKeypairJoinState(
+func verifyBoundKeypairJoinState(
ctx context.Context,
- log *slog.Logger,
- req *proto.RegisterUsingBoundKeypairInitialRequest,
+ params *JoinParams,
ptv2 *types.ProvisionTokenV2,
ca types.CertAuthority,
-) error {
+) (previousBotInstnceID string, err error) {
recoveryMode, err := boundkeypair.ParseRecoveryMode(ptv2.Spec.BoundKeypair.Recovery.Mode)
if err != nil {
- return trace.Wrap(err, "parsing recovery mode")
+ return "", trace.Wrap(err, "parsing recovery mode")
}
// Join state is required after the initial join (first recovery), so long
@@ -602,45 +466,41 @@ func (a *Server) verifyBoundKeypairJoinState(
// no client intervention.
joinStateRequired := ptv2.Status.BoundKeypair.RecoveryCount > 0 && recoveryMode != boundkeypair.RecoveryModeInsecure
if !joinStateRequired {
- log.DebugContext(
+ params.Logger.DebugContext(
ctx,
"skipping join state verification, not required due to token state",
"recovery_count", ptv2.Status.BoundKeypair.RecoveryCount,
"recovery_mode", ptv2.Spec.BoundKeypair.Recovery.Mode,
)
- return nil
+ return "", nil
}
// If join state is required but missing, raise an error.
- hasIncomingJoinState := len(req.PreviousJoinState) > 0
+ hasIncomingJoinState := len(params.BoundKeypairInit.PreviousJoinState) > 0
if !hasIncomingJoinState {
- return trace.AccessDenied("previous join state is required but was not provided")
+ return "", trace.AccessDenied("previous join state is required but was not provided")
}
- log.DebugContext(ctx, "join state verification required, verifying")
+ params.Logger.DebugContext(ctx, "join state verification required, verifying")
joinState, err := boundkeypair.VerifyJoinState(
ca,
- string(req.PreviousJoinState),
+ string(params.BoundKeypairInit.PreviousJoinState),
&boundkeypair.JoinStateParams{
- Clock: a.clock,
+ Clock: params.Clock,
ClusterName: ca.GetClusterName(), // equivalent to clusterName but saves a method param
Token: ptv2,
},
)
if err != nil {
- log.ErrorContext(ctx, "bound keypair join state verification failed", "error", err)
- a.tryLockBotInvalidJoinState(ctx, ptv2, req, err)
+ params.Logger.ErrorContext(ctx, "bound keypair join state verification failed", "error", err)
+ tryLockBotInvalidJoinState(ctx, params, ptv2, err)
- return trace.AccessDenied("join state verification failed")
+ return "", trace.AccessDenied("join state verification failed")
}
- // Now that we've verified it, make sure the previous bot instance ID is
- // passed along to generateCerts. This will only be used if a new bot
- // instance is generated.
- req.JoinRequest.PreviousBotInstanceID = joinState.BotInstanceID
-
- log.DebugContext(ctx, "join state verified successfully", "join_state", joinState)
- return nil
+ params.Logger.DebugContext(ctx, "join state verified successfully", "join_state", joinState)
+ // Now that we've verified it, return the previous bot instance ID.
+ return joinState.BotInstanceID, nil
}
// verifyLocksForBoundKeypairToken checks if any token-level locks are in place
@@ -648,62 +508,123 @@ func (a *Server) verifyBoundKeypairJoinState(
// been authenticated (exact criteria varies depending on token state) but
// before the request has mutated anything on the server - including creation of
// additional locks: we don't want to allow continuous lock creation.
-func (a *Server) verifyLocksForBoundKeypairToken(ctx context.Context, token *types.ProvisionTokenV2) error {
- readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
+func verifyLocksForBoundKeypairToken(ctx context.Context, params *JoinParams, token *types.ProvisionTokenV2) error {
+ readOnlyAuthPref, err := params.AuthService.GetReadOnlyAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}
- return trace.Wrap(a.checkLockInForce(
+ return trace.Wrap(params.AuthService.CheckLockInForce(
readOnlyAuthPref.GetLockingMode(),
[]types.LockTarget{{JoinToken: token.GetName()}},
))
}
-// RegisterUsingBoundKeypairMethod handles joining requests for the bound
-// keypair join method. If successful, returns a certificate bundle and client
-// joining parameters for use in subsequent join attempts.
-func (a *Server) RegisterUsingBoundKeypairMethod(
- ctx context.Context,
- req *proto.RegisterUsingBoundKeypairInitialRequest,
- challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
-) (_ *client.BoundKeypairRegistrationResponse, err error) {
- var provisionToken types.ProvisionToken
- var joinFailureMetadata any
- defer func() {
- // Emit a log message and audit event on join failure.
- if err != nil {
- a.handleJoinFailure(
- ctx, err, provisionToken, joinFailureMetadata, req.JoinRequest,
- )
+// JoinParams holds all parameters necessary to handle a bound keypair join attempt.
+type JoinParams struct {
+ // AuthService is the auth service.
+ AuthService AuthService
+ // AuthCtx is authentication context for the request, relevant for re-join attempts.
+ AuthCtx *authz.Context
+ // Diag is the join attempt diagnostic.
+ Diag *diagnostic.Diagnostic
+ // ProvisionToken is the provision token used for the join attempt.
+ ProvisionToken types.ProvisionToken
+ // ClientInit is the ClientInit message sent by the joining client.
+ ClientInit *messages.ClientInit
+ // BoundKeypairInit is the BoundKeypairInit message sent by the joining client.
+ BoundKeypairInit *messages.BoundKeypairInit
+ // IssueChallenge sends a challenge to the joining client and returns the
+ // response.
+ IssueChallenge ChallengeResponseFunc
+ // IssueRotationRequest sends a rotation request to the joining client and
+ // returns the response.
+ IssueRotationRequest RotationFunc
+ // CreateBoundKeypairValidator is a function that creates a bound keypair
+ // validator, used to override the validator in tests.
+ CreateBoundKeypairValidator CreateBoundKeypairValidator
+ // GenerateBotCerts is a function that generates bot certificates.
+ GenerateBotCerts func(ctx context.Context, previousBotInstanceID string, claims any) (*messages.Certificates, string, error)
+ // Clock is the clock.
+ Clock clockwork.Clock
+ // Logger is a logger.
+ Logger *slog.Logger
+}
+
+// ChallengeResponseFunc is function that sends a bound keypair challenge and
+// returns the response.
+type ChallengeResponseFunc func(*messages.BoundKeypairChallenge) (*messages.BoundKeypairChallengeSolution, error)
+
+// RotationFunc is function that sends a bound keypair rotation request and
+// returns the response.
+type RotationFunc func(*messages.BoundKeypairRotationRequest) (*messages.BoundKeypairRotationResponse, error)
+
+func (p *JoinParams) checkAndSetDefaults() error {
+ switch {
+ case p.AuthService == nil:
+ return trace.BadParameter("AuthService is required")
+ case p.AuthCtx == nil:
+ return trace.BadParameter("AuthCtx is required")
+ case p.Diag == nil:
+ return trace.BadParameter("Diag is required")
+ case p.ProvisionToken == nil:
+ return trace.BadParameter("ProvisionToken is required")
+ case p.ClientInit == nil:
+ return trace.BadParameter("ClientInit is required")
+ case p.BoundKeypairInit == nil:
+ return trace.BadParameter("BoundKeypairInit is required")
+ case p.IssueChallenge == nil:
+ return trace.BadParameter("IssueChallenge is required")
+ case p.IssueRotationRequest == nil:
+ return trace.BadParameter("IssueRotationRequest is required")
+ case p.Logger == nil:
+ return trace.BadParameter("Logger is required")
+ }
+ if p.CreateBoundKeypairValidator == nil {
+ p.CreateBoundKeypairValidator = func(subject string, clusterName string, publicKey crypto.PublicKey) (BoundKeypairValidator, error) {
+ return boundkeypair.NewChallengeValidator(subject, clusterName, publicKey)
}
- }()
+ }
+ if p.Clock == nil {
+ p.Clock = clockwork.NewRealClock()
+ }
+ return nil
+}
+
+// AuthService is the subset of the Auth service interface required to implement bound keypair joining.
+type AuthService interface {
+ EmitAuditEvent(ctx context.Context, e apievents.AuditEvent) error
+ GetClusterName(context.Context) (types.ClusterName, error)
+ GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error)
+ GetKeyStore() *keystore.Manager
+ PatchToken(context.Context, string, func(types.ProvisionToken) (types.ProvisionToken, error)) (types.ProvisionToken, error)
+ GetAuthPreference(context.Context) (types.AuthPreference, error)
+ GetReadOnlyAuthPreference(context.Context) (readonly.AuthPreference, error)
+ UpsertLock(context.Context, types.Lock) error
+ CheckLockInForce(constants.LockingMode, []types.LockTarget) error
+}
- // First, check the specified token exists, and is a bound keypair-type join
- // token.
- if err := req.JoinRequest.CheckAndSetDefaults(); err != nil {
+// HandleBoundKeypairJoin handles joining requests for the bound keypair join
+// method.
+func HandleBoundKeypairJoin(
+ ctx context.Context,
+ params *JoinParams,
+) (*messages.BotResult, error) {
+ if err := params.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
-
// Only bot joining is supported at the moment - unique ID verification is
// required and this is currently only implemented for bots.
- if req.JoinRequest.Role != types.RoleBot {
+ if types.SystemRole(params.ClientInit.SystemRole) != types.RoleBot {
return nil, trace.BadParameter("bound keypair joining is only supported for bots")
}
- provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req.JoinRequest)
- if err != nil {
- return nil, trace.Wrap(err)
- }
- ptv2, ok := provisionToken.(*types.ProvisionTokenV2)
+ ptv2, ok := params.ProvisionToken.(*types.ProvisionTokenV2)
if !ok {
- return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", provisionToken)
- }
- if ptv2.Spec.JoinMethod != types.JoinMethodBoundKeypair {
- return nil, trace.BadParameter("specified join token is not for `%s` method", types.JoinMethodBoundKeypair)
+ return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", params.ProvisionToken)
}
- log := a.logger.With("token", ptv2.GetName())
+ log := params.Logger.With("token", ptv2.GetName())
if ptv2.Status == nil {
ptv2.Status = &types.ProvisionTokenStatusV2{}
@@ -712,7 +633,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
ptv2.Status.BoundKeypair = &types.ProvisionTokenStatusV2BoundKeypair{}
}
- clusterName, err := a.GetClusterName(ctx)
+ clusterName, err := params.AuthService.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -721,7 +642,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
status := ptv2.Status.BoundKeypair
hasBoundPublicKey := status.BoundPublicKey != ""
hasBoundBotInstance := status.BoundBotInstanceID != ""
- hasIncomingBotInstance := req.JoinRequest.BotInstanceID != ""
+ hasIncomingBotInstance := params.AuthCtx.BotInstanceID != ""
hasJoinsRemaining := status.RecoveryCount < spec.Recovery.Limit
recoveryMode, err := boundkeypair.ParseRecoveryMode(spec.Recovery.Mode)
@@ -745,7 +666,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
var mutators []boundKeypairStatusMutator
// Get the join state JWT signer CA
- ca, err := a.GetCertAuthority(ctx, types.CertAuthID{
+ ca, err := params.AuthService.GetCertAuthority(ctx, types.CertAuthID{
Type: types.BoundKeypairCA,
DomainName: clusterName.GetClusterName(),
}, /* loadKeys */ true)
@@ -753,6 +674,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
return nil, trace.Wrap(err)
}
+ var verifiedPreviousBotInstanceID string
switch {
case !hasBoundPublicKey && !hasIncomingBotInstance:
// Normal initial join attempt. No bound key, and no incoming bot
@@ -764,13 +686,13 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
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(
+ if err := issueBoundKeypairChallenge(
ctx,
+ params,
spec.Onboarding.InitialPublicKey,
- challengeResponse,
); err != nil {
log.WarnContext(ctx, "denying initial join attempt, client failed to complete challenge", "error", err)
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, spec.Onboarding.InitialPublicKey, 0, err)
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, spec.Onboarding.InitialPublicKey, 0, err)
return nil, trace.AccessDenied("failed to complete challenge")
}
@@ -786,14 +708,14 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
const errMsg = "a valid registration secret is required"
// A registration secret is expected.
- if req.InitialJoinSecret == "" {
+ if params.BoundKeypairInit.InitialJoinSecret == "" {
log.WarnContext(ctx, "denying join attempt, client failed to provide required registration secret")
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, "", 0, trace.AccessDenied("no registration secret was provided"))
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, "", 0, trace.AccessDenied("no registration secret was provided"))
return nil, trace.AccessDenied(errMsg)
}
if spec.Onboarding.MustRegisterBefore != nil {
- if a.clock.Now().After(*spec.Onboarding.MustRegisterBefore) {
+ if params.Clock.Now().After(*spec.Onboarding.MustRegisterBefore) {
log.WarnContext(
ctx,
"denying join attempt due to expired registration secret",
@@ -805,18 +727,18 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
}
// Verify the secret.
- if subtle.ConstantTimeCompare([]byte(status.RegistrationSecret), []byte(req.InitialJoinSecret)) != 1 {
+ if subtle.ConstantTimeCompare([]byte(status.RegistrationSecret), []byte(params.BoundKeypairInit.InitialJoinSecret)) != 1 {
log.WarnContext(ctx, "denying join attempt, client provided incorrect registration secret")
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, "", 0, trace.AccessDenied("registration secret comparison failed"))
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, "", 0, trace.AccessDenied("registration secret comparison failed"))
return nil, trace.AccessDenied(errMsg)
}
// Ask the client for a new public key.
- newPubKey, err := a.requestBoundKeypairRotation(ctx, challengeResponse)
+ newPubKey, err := requestBoundKeypairRotation(ctx, params)
if err != nil {
// Audit note: `requestBoundKeypairRotation()` will also emit an
// audit event.
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, "", 0, err)
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, "", 0, err)
return nil, trace.Wrap(err, "requesting public key")
}
@@ -847,7 +769,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// request. We don't want to leak the lock status to random
// unauthenticated clients, and by this point, we haven't mutated any
// server-side state.
- if err := a.verifyLocksForBoundKeypairToken(ctx, ptv2); err != nil {
+ if err := verifyLocksForBoundKeypairToken(ctx, params, ptv2); err != nil {
return nil, trace.Wrap(err)
}
@@ -855,7 +777,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
recoveryCount += 1
expectNewBotInstance = true
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, boundPublicKey, recoveryCount, nil)
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, boundPublicKey, recoveryCount, nil)
case !hasBoundPublicKey && hasIncomingBotInstance:
// Not allowed, at least at the moment. This would imply e.g. trying to
// change auth methods.
@@ -867,10 +789,10 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
return nil, trace.BadParameter("bad backend state, please recreate the join token")
case hasBoundPublicKey && hasBoundBotInstance && hasIncomingBotInstance:
// Standard rejoin case, does not consume a rejoin.
- if err := a.issueBoundKeypairChallenge(
+ if err := issueBoundKeypairChallenge(
ctx,
+ params,
status.BoundPublicKey,
- challengeResponse,
); err != nil {
return nil, trace.Wrap(err)
}
@@ -878,7 +800,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// Verify locks here now that we've verified private key ownership but
// before we check join state. Otherwise, we could allow a lock creation
// loop.
- if err := a.verifyLocksForBoundKeypairToken(ctx, ptv2); err != nil {
+ if err := verifyLocksForBoundKeypairToken(ctx, params, ptv2); err != nil {
return nil, trace.Wrap(err)
}
@@ -887,7 +809,8 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// make sure an otherwise unauthorized client can't trigger a lockout.
// This also needs to be done before rotation to prevent an attacker
// from rotating the key.
- if err := a.verifyBoundKeypairJoinState(ctx, log, req, ptv2, ca); err != nil {
+ verifiedPreviousBotInstanceID, err = verifyBoundKeypairJoinState(ctx, params, ptv2, ca)
+ if err != nil {
return nil, trace.AccessDenied("join state verification failed")
}
@@ -899,19 +822,19 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// any event that might have cycled bot instance IDs should have also
// modified the join state causing a failure above. In any case, we'll
// keep this as a sanity check.
- if status.BoundBotInstanceID != req.JoinRequest.BotInstanceID {
+ if status.BoundBotInstanceID != params.AuthCtx.BotInstanceID {
return nil, trace.AccessDenied("bot instance mismatch")
}
// Nothing else to do, no key change, no additional audit event; regular
// bot join event will be emitted later.
case hasBoundPublicKey && hasBoundBotInstance && !hasIncomingBotInstance:
- if err := a.issueBoundKeypairChallenge(
+ if err := issueBoundKeypairChallenge(
ctx,
+ params,
status.BoundPublicKey,
- challengeResponse,
); err != nil {
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, boundPublicKey, recoveryCount, err)
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, boundPublicKey, recoveryCount, err)
return nil, trace.Wrap(err)
}
@@ -925,13 +848,14 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// Verify locks here now that we've verified private key ownership but
// before we check join state. Otherwise, we could allow a lock creation
// loop.
- if err := a.verifyLocksForBoundKeypairToken(ctx, ptv2); err != nil {
+ if err := verifyLocksForBoundKeypairToken(ctx, params, ptv2); err != nil {
return nil, trace.Wrap(err)
}
// As in the standard case above, once we've verified the client has the
// matching private key, validate the join state.
- if err := a.verifyBoundKeypairJoinState(ctx, log, req, ptv2, ca); err != nil {
+ verifiedPreviousBotInstanceID, err = verifyBoundKeypairJoinState(ctx, params, ptv2, ca)
+ if err != nil {
return nil, trace.AccessDenied("join state verification failed")
}
@@ -942,7 +866,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
recoveryCount += 1
expectNewBotInstance = true
- a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, boundPublicKey, recoveryCount, nil)
+ emitBoundKeypairRecoveryEvent(ctx, params, ptv2, boundPublicKey, recoveryCount, nil)
default:
log.ErrorContext(
ctx, "unexpected state",
@@ -956,22 +880,22 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
}
// If we've crossed a keypair rotation threshold, request one now.
- now := a.clock.Now()
+ now := params.Clock.Now()
if shouldRequestBoundKeypairRotation(spec.RotateAfter, status.LastRotatedAt, now) {
log.DebugContext(
ctx, "requesting keypair rotation",
"rotate_after", formatTimePointer(spec.RotateAfter),
"last_rotated_at", formatTimePointer(status.LastRotatedAt),
)
- newPubKey, err := a.requestBoundKeypairRotation(ctx, challengeResponse)
+ newPubKey, err := requestBoundKeypairRotation(ctx, params)
if err != nil {
- a.emitBoundKeypairRotationEvent(ctx, req, ptv2, boundPublicKey, "", err)
+ emitBoundKeypairRotationEvent(ctx, params, ptv2, boundPublicKey, "", err)
return nil, trace.Wrap(err)
}
// Don't let clients provide the same key again.
if err := ensurePublicKeysNotEqual(boundPublicKey, newPubKey); err != nil {
- a.emitBoundKeypairRotationEvent(ctx, req, ptv2, boundPublicKey, newPubKey, err)
+ emitBoundKeypairRotationEvent(ctx, params, ptv2, boundPublicKey, newPubKey, err)
return nil, trace.Wrap(err)
}
@@ -980,20 +904,15 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
mutateStatusLastRotatedAt(&now, status.LastRotatedAt),
)
- a.emitBoundKeypairRotationEvent(ctx, req, ptv2, boundPublicKey, newPubKey, nil)
+ emitBoundKeypairRotationEvent(ctx, params, ptv2, boundPublicKey, newPubKey, nil)
boundPublicKey = newPubKey
}
- params := makeBotCertsParams(
- req.JoinRequest,
- &boundkeypair.Claims{
- PublicKey: boundPublicKey,
- RecoveryCount: recoveryCount,
- RecoveryMode: recoveryMode,
- },
- nil, // TODO: workload id claims
- )
- certs, botInstanceID, err := a.GenerateBotCertsForJoin(ctx, provisionToken, params)
+ certs, botInstanceID, err := params.GenerateBotCerts(ctx, verifiedPreviousBotInstanceID, &boundkeypair.Claims{
+ PublicKey: boundPublicKey,
+ RecoveryCount: recoveryCount,
+ RecoveryMode: recoveryMode,
+ })
if err != nil {
return nil, trace.Wrap(err)
}
@@ -1010,10 +929,10 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
finalToken := ptv2
if len(mutators) > 0 {
- patched, err := a.PatchToken(ctx, ptv2.GetName(), func(token types.ProvisionToken) (types.ProvisionToken, error) {
- ptv2, ok := provisionToken.(*types.ProvisionTokenV2)
+ patched, err := params.AuthService.PatchToken(ctx, ptv2.GetName(), func(token types.ProvisionToken) (types.ProvisionToken, error) {
+ ptv2, ok := params.ProvisionToken.(*types.ProvisionTokenV2)
if !ok {
- return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", provisionToken)
+ return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", params.ProvisionToken)
}
// Apply all mutators. Individual mutators may make additional
@@ -1035,17 +954,17 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// This should be impossible, but if it did fail, we can't generate
// a join state without an accurate token. The certs we just
// generated will be useless, so just return an error.
- return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", provisionToken)
+ return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", params.ProvisionToken)
}
}
- signer, err := a.GetKeyStore().GetJWTSigner(ctx, ca)
+ signer, err := params.AuthService.GetKeyStore().GetJWTSigner(ctx, ca)
if err != nil {
return nil, trace.Wrap(err, "issuing join state document")
}
newJoinState, err := boundkeypair.IssueJoinState(signer, &boundkeypair.JoinStateParams{
- Clock: a.clock,
+ Clock: params.Clock,
ClusterName: clusterName.GetClusterName(),
Token: finalToken,
})
@@ -1053,9 +972,11 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
return nil, trace.Wrap(err, "issuing join state document")
}
- return &client.BoundKeypairRegistrationResponse{
- Certs: certs,
- BoundPublicKey: boundPublicKey,
- JoinState: []byte(newJoinState),
+ return &messages.BotResult{
+ Certificates: *certs,
+ BoundKeypairResult: &messages.BoundKeypairResult{
+ JoinState: []byte(newJoinState),
+ PublicKey: []byte(boundPublicKey),
+ },
}, nil
}
diff --git a/lib/join/internal/messages/messages.go b/lib/join/internal/messages/messages.go
index 1f6071711cac2..be8c144c32ff3 100644
--- a/lib/join/internal/messages/messages.go
+++ b/lib/join/internal/messages/messages.go
@@ -192,6 +192,98 @@ func (k *PublicKeys) check() error {
return nil
}
+// BoundKeypairInit is sent from the client in response to the ServerInit
+// message for the bound keypair join method.
+// The server is expected to respond with a BoundKeypairChallenge.
+//
+// The bound keypair method join flow is:
+// 1. client->server: ClientInit
+// 2. server->client: ServerInit
+// 3. client->server: BoundKeypairInit
+// 4. server->client: BoundKeypairChallenge
+// 5. client->server: BoundKeypairChallengeSolution
+// (optional additional steps if keypair rotation is required)
+// server->client: BoundKeypairRotationRequest
+// client->server: BoundKeypairRotationResponse
+// server->client: BoundKeypairChallenge
+// client->server: BoundKeypairChallengeSolution
+// 6. server->client: Result containing BoundKeypairResult
+type BoundKeypairInit struct {
+ embedRequest
+
+ // ClientParams holds parameters for the specific type of client trying to join.
+ ClientParams ClientParams
+ // If set, attempts to bind a new keypair using an initial join secret. Any
+ // value set here will be ignored if a keypair is already bound.
+ InitialJoinSecret string
+ // A document signed by Auth containing join state parameters from the
+ // previous join attempt. Not required on initial join; required on all
+ // subsequent joins.
+ PreviousJoinState []byte
+}
+
+// BoundKeypairChallenge is a challenge issued by the server that joining
+// clients are expected to complete.
+// The client is expected to respond with a BoundKeypairChallengeSolution.
+type BoundKeypairChallenge struct {
+ embedResponse
+
+ // The desired public key corresponding to the private key that should be
+ // used to sign this challenge, in SSH authorized keys format.
+ PublicKey []byte
+ // A challenge to sign with the requested public key. During keypair
+ // rotation, a second challenge will be provided to verify the new keypair
+ // before certs are returned.
+ Challenge string
+}
+
+// BoundKeypairChallengeSolution is sent from the client in response to the
+// BoundKeypairChallenge.
+// The server is expected to respond with either a Result or a
+// BoundKeypairRotationRequest.
+type BoundKeypairChallengeSolution struct {
+ embedRequest
+
+ // A solution to a challenge from the server. This generated by signing the
+ // challenge as a JWT using the keypair associated with the requested public
+ // key.
+ Solution []byte
+}
+
+// BoundKeypairRotationRequest is sent by the server in response to a
+// BoundKeypairChallenge when a keypair rotation is required. It acts like an
+// additional challenge, the client is expected to respond with a
+// BoundKeypairRotationResponse.
+type BoundKeypairRotationRequest struct {
+ embedResponse
+
+ // The signature algorithm suite in use by the cluster.
+ SignatureAlgorithmSuite string
+}
+
+// BoundKeypairRotationResponse is sent by the client in response to a
+// BoundKeypairRotationRequest from the server. The server is expected to
+// respond with an additional BoundKeypairChallenge for the new key.
+type BoundKeypairRotationResponse struct {
+ embedRequest
+
+ // The public key to be registered with auth. Clients should expect a
+ // subsequent challenge against this public key to be sent. This is encoded
+ // in SSH authorized keys format.
+ PublicKey []byte
+}
+
+// BoundKeypairResult holds additional result parameters relevant to the bound
+// keypair join method.
+type BoundKeypairResult struct {
+ // A signed join state document to be provided on the next join attempt.
+ JoinState []byte
+ // The public key registered with Auth at the end of the joining ceremony.
+ // After a successful keypair rotation, this should reflect the newly
+ // registered public key. This is encoded in SSH authorized keys format.
+ PublicKey []byte
+}
+
// Response is implemented by all join response messages.
type Response interface {
isResponse()
@@ -231,6 +323,8 @@ type BotResult struct {
// Certificates holds issued certificates and cluster CAs.
Certificates Certificates
+ // BoundKeypairResult holds extra result parameters relevant to the bound keypair join method.
+ BoundKeypairResult *BoundKeypairResult
}
// Certificates holds issued certificates and cluster CAs.
diff --git a/lib/join/joinclient/join.go b/lib/join/joinclient/join.go
index d20b35777576b..51e63e1fc2e48 100644
--- a/lib/join/joinclient/join.go
+++ b/lib/join/joinclient/join.go
@@ -168,43 +168,54 @@ func joinWithClient(ctx context.Context, params JoinParams, client *joinv1.Clien
clientParams := makeClientParams(params, publicKeys)
// Delegate out to the handler for the specific join method.
- if err := joinWithMethod(stream, clientParams, serverInit.JoinMethod); err != nil {
+ resultMsg, err := joinWithMethod(ctx, stream, params, clientParams, serverInit.JoinMethod)
+ if err != nil {
return nil, trace.Wrap(err)
}
- // Receive the final result message.
- if params.ID.Role == types.RoleBot {
- botResult, err := messages.RecvResponse[*messages.BotResult](stream)
+ // Convert the result message into a JoinResult.
+ switch typedResult := resultMsg.(type) {
+ case *messages.HostResult:
+ return makeJoinResult(signer, typedResult.Certificates)
+ case *messages.BotResult:
+ joinResult, err := makeJoinResult(signer, typedResult.Certificates)
if err != nil {
return nil, trace.Wrap(err)
}
- return makeJoinResult(signer, botResult.Certificates)
- }
- hostResult, err := messages.RecvResponse[*messages.HostResult](stream)
- if err != nil {
- return nil, trace.Wrap(err)
+ if typedResult.BoundKeypairResult != nil {
+ joinResult.BoundKeypair = &authjoin.BoundKeypairRegisterResult{
+ BoundPublicKey: string(typedResult.BoundKeypairResult.PublicKey),
+ JoinState: typedResult.BoundKeypairResult.JoinState,
+ }
+ }
+ return joinResult, nil
+ default:
+ return nil, trace.BadParameter("unhandled result message type %T", resultMsg)
}
- return makeJoinResult(signer, hostResult.Certificates)
}
func joinWithMethod(
+ ctx context.Context,
stream messages.ClientStream,
+ joinParams JoinParams,
clientParams messages.ClientParams,
method string,
-) error {
+) (messages.Response, error) {
switch types.JoinMethod(method) {
case types.JoinMethodToken:
- return trace.Wrap(tokenJoin(stream, clientParams))
+ return tokenJoin(stream, clientParams)
+ case types.JoinMethodBoundKeypair:
+ return boundKeypairJoin(ctx, stream, joinParams, clientParams)
default:
// TODO(nklaassen): implement remaining join methods.
- return trace.NotImplemented("server selected join method %v which is not supported by this client", method)
+ return nil, trace.NotImplemented("server selected join method %v which is not supported by this client", method)
}
}
func tokenJoin(
stream messages.ClientStream,
clientParams messages.ClientParams,
-) error {
+) (messages.Response, error) {
// The token join method is relatively simple, the flow is
//
// client->server ClientInit
@@ -213,12 +224,16 @@ func tokenJoin(
// client<-server Result
//
// At this point the ServerInit messages has already been received, all
- // that's left is to send the TokenInit message, the caller will handle
- // receiving the final result.
+ // that's left is to send the TokenInit message and receive the final result.
tokenInitMsg := &messages.TokenInit{
ClientParams: clientParams,
}
- return trace.Wrap(stream.Send(tokenInitMsg))
+ if err := stream.Send(tokenInitMsg); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ // Receive and return the final result.
+ result, err := stream.Recv()
+ return result, trace.Wrap(err)
}
func makeClientParams(params JoinParams, publicKeys *messages.PublicKeys) messages.ClientParams {
diff --git a/lib/join/joinclient/join_boundkeypair.go b/lib/join/joinclient/join_boundkeypair.go
new file mode 100644
index 0000000000000..222e304bf0551
--- /dev/null
+++ b/lib/join/joinclient/join_boundkeypair.go
@@ -0,0 +1,162 @@
+// Teleport
+// Copyright (C) 2025 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 .
+
+package joinclient
+
+import (
+ "context"
+ "crypto"
+ "log/slog"
+ "strings"
+
+ "github.com/go-jose/go-jose/v3"
+ "github.com/gravitational/trace"
+ "golang.org/x/crypto/ssh"
+
+ "github.com/gravitational/teleport/api/types"
+ authjoin "github.com/gravitational/teleport/lib/auth/join"
+ "github.com/gravitational/teleport/lib/cryptosuites"
+ "github.com/gravitational/teleport/lib/join/internal/messages"
+ "github.com/gravitational/teleport/lib/jwt"
+)
+
+type (
+ BoundKeypairParams = authjoin.BoundKeypairParams
+ GetSignerFunc = authjoin.GetSignerFunc
+ KeygenFunc = authjoin.KeygenFunc
+ BoundKeypairResult = authjoin.BoundKeypairRegisterResult
+)
+
+func boundKeypairJoin(
+ ctx context.Context,
+ stream messages.ClientStream,
+ joinParams JoinParams,
+ clientParams messages.ClientParams,
+) (messages.Response, error) {
+ // The bound keypair join method is relatively complex compared to other
+ // join methods, the flow is:
+ //
+ // client->server ClientInit
+ // client<-server ServerInit
+ // client->server BoundKeypairInit
+ // client<-server BoundKeypairChallenge
+ // client->server BoundKeypairChallengeSolution
+ // (optional additional steps if keypair rotation is required)
+ // client<-server: BoundKeypairRotationRequest
+ // client->server: BoundKeypairRotationResponse
+ // client<-server: BoundKeypairChallenge
+ // client->server: BoundKeypairChallengeSolution
+ // client<-server: Result containing BoundKeypairResult
+ //
+ // At this point the ServerInit message has already been received, this
+ // function needs to send the BoundKeyPairInit and then handle any
+ // challenges and rotation requests.
+ boundKeypairInit := &messages.BoundKeypairInit{
+ ClientParams: clientParams,
+ InitialJoinSecret: joinParams.BoundKeypairParams.InitialJoinSecret,
+ PreviousJoinState: joinParams.BoundKeypairParams.PreviousJoinState,
+ }
+ if err := stream.Send(boundKeypairInit); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ bkParams := joinParams.BoundKeypairParams
+ for {
+ msg, err := stream.Recv()
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ switch kind := msg.(type) {
+ case *messages.BotResult:
+ // Return the final result.
+ return kind, nil
+ case *messages.BoundKeypairChallenge:
+ signer, err := bkParams.GetSigner(string(kind.PublicKey))
+ if err != nil {
+ return nil, trace.Wrap(err, "could not lookup signer for public key %+v", string(kind.PublicKey))
+ }
+
+ alg, err := jwt.AlgorithmForPublicKey(signer.Public())
+ if err != nil {
+ return nil, trace.Wrap(err, "determining signing algorithm for public key")
+ }
+
+ opts := (&jose.SignerOptions{}).WithType("JWT")
+ key := jose.SigningKey{
+ Algorithm: alg,
+ Key: signer,
+ }
+
+ joseSigner, err := jose.NewSigner(key, opts)
+ if err != nil {
+ return nil, trace.Wrap(err, "creating signer")
+ }
+
+ jws, err := joseSigner.Sign([]byte(kind.Challenge))
+ if err != nil {
+ return nil, trace.Wrap(err, "signing challenge")
+ }
+
+ serialized, err := jws.CompactSerialize()
+ if err != nil {
+ return nil, trace.Wrap(err, "serializing signed challenge")
+ }
+ if err := stream.Send(&messages.BoundKeypairChallengeSolution{
+ Solution: []byte(serialized),
+ }); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ case *messages.BoundKeypairRotationRequest:
+ if bkParams.RequestNewKeypair == nil {
+ return nil, trace.BadParameter("RequestNewKeypair is required")
+ }
+
+ slog.InfoContext(ctx, "Server has requested keypair rotation", "suite", kind.SignatureAlgorithmSuite)
+
+ suite, err := types.SignatureAlgorithmSuiteFromString(kind.SignatureAlgorithmSuite)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ newSigner, err := bkParams.RequestNewKeypair(ctx, cryptosuites.StaticAlgorithmSuite(suite))
+ if err != nil {
+ return nil, trace.Wrap(err, "requesting new keypair")
+ }
+
+ newPubkey, err := sshPubKeyFromSigner(newSigner)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ if err := stream.Send(&messages.BoundKeypairRotationResponse{
+ PublicKey: []byte(newPubkey),
+ }); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ default:
+ return nil, trace.Errorf("server sent unexpected message type %T", msg)
+ }
+ }
+}
+
+// sshPubKeyFromSigner returns the public key of the given signer in ssh
+// authorized_keys format.
+func sshPubKeyFromSigner(signer crypto.Signer) (string, error) {
+ sshKey, err := ssh.NewPublicKey(signer.Public())
+ if err != nil {
+ return "", trace.Wrap(err, "creating SSH public key from signer")
+ }
+
+ return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshKey))), nil
+}
diff --git a/lib/join/joinv1/messages.go b/lib/join/joinv1/messages.go
index 685aa902b2381..e8467262b21e7 100644
--- a/lib/join/joinv1/messages.go
+++ b/lib/join/joinv1/messages.go
@@ -32,6 +32,10 @@ func requestToMessage(req *joinv1.JoinRequest) (messages.Request, error) {
return clientInitToMessage(msg.ClientInit), nil
case *joinv1.JoinRequest_TokenInit:
return tokenInitToMessage(msg.TokenInit)
+ case *joinv1.JoinRequest_BoundKeypairInit:
+ return boundKeypairInitToMessage(msg.BoundKeypairInit)
+ case *joinv1.JoinRequest_Solution:
+ return challengeSolutionToMessage(msg.Solution)
default:
return nil, trace.BadParameter("unrecognized join request message type %T", msg)
}
@@ -57,6 +61,26 @@ func requestFromMessage(msg messages.Request) (*joinv1.JoinRequest, error) {
TokenInit: tokenInit,
},
}, nil
+ case *messages.BoundKeypairInit:
+ boundKeypairInit, err := boundKeypairInitFromMessage(typedMsg)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &joinv1.JoinRequest{
+ Payload: &joinv1.JoinRequest_BoundKeypairInit{
+ BoundKeypairInit: boundKeypairInit,
+ },
+ }, nil
+ case *messages.BoundKeypairChallengeSolution, *messages.BoundKeypairRotationResponse:
+ solution, err := challengeSolutionFromMessage(typedMsg)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &joinv1.JoinRequest{
+ Payload: &joinv1.JoinRequest_Solution{
+ Solution: solution,
+ },
+ }, nil
default:
return nil, trace.BadParameter("unrecognized join request message type %T", msg)
}
@@ -197,15 +221,47 @@ func publicKeysFromMessage(msg messages.PublicKeys) *joinv1.PublicKeys {
}
}
+func challengeSolutionToMessage(req *joinv1.ChallengeSolution) (messages.Request, error) {
+ switch payload := req.GetPayload().(type) {
+ case *joinv1.ChallengeSolution_BoundKeypairChallengeSolution:
+ return boundKeypairChallengeSolutionToMessage(payload.BoundKeypairChallengeSolution), nil
+ case *joinv1.ChallengeSolution_BoundKeypairRotationResponse:
+ return boundKeypairRotationResponseToMessage(payload.BoundKeypairRotationResponse), nil
+ default:
+ return nil, trace.BadParameter("unrecognized challenge solution message type %T", payload)
+ }
+}
+
+func challengeSolutionFromMessage(msg messages.Request) (*joinv1.ChallengeSolution, error) {
+ switch typedMsg := msg.(type) {
+ case *messages.BoundKeypairChallengeSolution:
+ return &joinv1.ChallengeSolution{
+ Payload: &joinv1.ChallengeSolution_BoundKeypairChallengeSolution{
+ BoundKeypairChallengeSolution: boundKeypairChallengeSolutionFromMessage(typedMsg),
+ },
+ }, nil
+ case *messages.BoundKeypairRotationResponse:
+ return &joinv1.ChallengeSolution{
+ Payload: &joinv1.ChallengeSolution_BoundKeypairRotationResponse{
+ BoundKeypairRotationResponse: boundKeypairRotationResponseFromMessage(typedMsg),
+ },
+ }, nil
+ default:
+ return nil, trace.BadParameter("unrecognized challenge solution message type %T", msg)
+ }
+}
+
// responseToMessage converts a gRPC JoinResponse into a protocol-agnostic [messages.Response].
func responseToMessage(resp *joinv1.JoinResponse) (messages.Response, error) {
switch typedResp := resp.Payload.(type) {
case *joinv1.JoinResponse_Init:
return serverInitToMessage(typedResp.Init)
+ case *joinv1.JoinResponse_Challenge:
+ return challengeToMessage(typedResp.Challenge)
case *joinv1.JoinResponse_Result:
return resultToMessage(typedResp.Result)
default:
- return nil, trace.BadParameter("unrecognized join responsed message type %T", typedResp)
+ return nil, trace.BadParameter("unrecognized join response message type %T", typedResp)
}
}
@@ -219,6 +275,16 @@ func responseFromMessage(msg messages.Response) (*joinv1.JoinResponse, error) {
Init: serverInitFromMessage(typedMsg),
},
}, nil
+ case *messages.BoundKeypairChallenge, *messages.BoundKeypairRotationRequest:
+ challenge, err := challengeFromMessage(msg)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &joinv1.JoinResponse{
+ Payload: &joinv1.JoinResponse_Challenge{
+ Challenge: challenge,
+ },
+ }, nil
case *messages.HostResult:
return &joinv1.JoinResponse{
Payload: &joinv1.JoinResponse_Result{
@@ -262,6 +328,36 @@ func serverInitFromMessage(msg *messages.ServerInit) *joinv1.ServerInit {
}
}
+func challengeToMessage(resp *joinv1.Challenge) (messages.Response, error) {
+ switch payload := resp.Payload.(type) {
+ case *joinv1.Challenge_BoundKeypairChallenge:
+ return boundKeypairChallengeToMessage(payload.BoundKeypairChallenge), nil
+ case *joinv1.Challenge_BoundKeypairRotationRequest:
+ return boundKeypairRotationRequestToMessage(payload.BoundKeypairRotationRequest), nil
+ default:
+ return nil, trace.BadParameter("unrecognized challenge payload type %T", payload)
+ }
+}
+
+func challengeFromMessage(resp messages.Response) (*joinv1.Challenge, error) {
+ switch msg := resp.(type) {
+ case *messages.BoundKeypairChallenge:
+ return &joinv1.Challenge{
+ Payload: &joinv1.Challenge_BoundKeypairChallenge{
+ BoundKeypairChallenge: boundKeypairChallengeFromMessage(msg),
+ },
+ }, nil
+ case *messages.BoundKeypairRotationRequest:
+ return &joinv1.Challenge{
+ Payload: &joinv1.Challenge_BoundKeypairRotationRequest{
+ BoundKeypairRotationRequest: boundKeypairRotationRequestFromMessage(msg),
+ },
+ }, nil
+ default:
+ return nil, trace.BadParameter("unrecognized challenge message type %T", msg)
+ }
+}
+
func resultToMessage(resp *joinv1.Result) (messages.Response, error) {
switch resp.Payload.(type) {
case *joinv1.Result_HostResult:
@@ -289,13 +385,15 @@ func hostResultFromMessage(msg *messages.HostResult) *joinv1.HostResult {
func botResultToMessage(resp *joinv1.BotResult) *messages.BotResult {
return &messages.BotResult{
- Certificates: certificatesToMessage(resp.Certificates),
+ Certificates: certificatesToMessage(resp.Certificates),
+ BoundKeypairResult: boundKeypairResultToMessage(resp.BoundKeypairResult),
}
}
func botResultFromMessage(msg *messages.BotResult) *joinv1.BotResult {
return &joinv1.BotResult{
- Certificates: certificatesFromMessage(&msg.Certificates),
+ Certificates: certificatesFromMessage(&msg.Certificates),
+ BoundKeypairResult: boundKeypairResultFromMessage(msg.BoundKeypairResult),
}
}
diff --git a/lib/join/joinv1/messages_boundkeypair.go b/lib/join/joinv1/messages_boundkeypair.go
new file mode 100644
index 0000000000000..32739453305d3
--- /dev/null
+++ b/lib/join/joinv1/messages_boundkeypair.go
@@ -0,0 +1,118 @@
+// Teleport
+// Copyright (C) 2025 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 .
+
+package joinv1
+
+import (
+ "github.com/gravitational/trace"
+
+ joinv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/join/v1"
+ "github.com/gravitational/teleport/lib/join/internal/messages"
+)
+
+func boundKeypairInitToMessage(req *joinv1.BoundKeypairInit) (*messages.BoundKeypairInit, error) {
+ clientParams, err := clientParamsToMessage(req.ClientParams)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &messages.BoundKeypairInit{
+ ClientParams: clientParams,
+ InitialJoinSecret: req.InitialJoinSecret,
+ PreviousJoinState: req.PreviousJoinState,
+ }, nil
+}
+
+func boundKeypairInitFromMessage(msg *messages.BoundKeypairInit) (*joinv1.BoundKeypairInit, error) {
+ clientParams, err := clientParamsFromMessage(msg.ClientParams)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &joinv1.BoundKeypairInit{
+ ClientParams: clientParams,
+ InitialJoinSecret: msg.InitialJoinSecret,
+ PreviousJoinState: msg.PreviousJoinState,
+ }, nil
+}
+
+func boundKeypairChallengeToMessage(resp *joinv1.BoundKeypairChallenge) *messages.BoundKeypairChallenge {
+ return &messages.BoundKeypairChallenge{
+ PublicKey: resp.PublicKey,
+ Challenge: resp.Challenge,
+ }
+}
+
+func boundKeypairChallengeFromMessage(msg *messages.BoundKeypairChallenge) *joinv1.BoundKeypairChallenge {
+ return &joinv1.BoundKeypairChallenge{
+ PublicKey: msg.PublicKey,
+ Challenge: msg.Challenge,
+ }
+}
+
+func boundKeypairChallengeSolutionToMessage(req *joinv1.BoundKeypairChallengeSolution) *messages.BoundKeypairChallengeSolution {
+ return &messages.BoundKeypairChallengeSolution{
+ Solution: req.Solution,
+ }
+}
+
+func boundKeypairRotationRequestToMessage(resp *joinv1.BoundKeypairRotationRequest) *messages.BoundKeypairRotationRequest {
+ return &messages.BoundKeypairRotationRequest{
+ SignatureAlgorithmSuite: resp.SignatureAlgorithmSuite,
+ }
+}
+
+func boundKeypairRotationRequestFromMessage(resp *messages.BoundKeypairRotationRequest) *joinv1.BoundKeypairRotationRequest {
+ return &joinv1.BoundKeypairRotationRequest{
+ SignatureAlgorithmSuite: resp.SignatureAlgorithmSuite,
+ }
+}
+
+func boundKeypairChallengeSolutionFromMessage(msg *messages.BoundKeypairChallengeSolution) *joinv1.BoundKeypairChallengeSolution {
+ return &joinv1.BoundKeypairChallengeSolution{
+ Solution: msg.Solution,
+ }
+}
+
+func boundKeypairRotationResponseToMessage(req *joinv1.BoundKeypairRotationResponse) *messages.BoundKeypairRotationResponse {
+ return &messages.BoundKeypairRotationResponse{
+ PublicKey: req.PublicKey,
+ }
+}
+
+func boundKeypairRotationResponseFromMessage(msg *messages.BoundKeypairRotationResponse) *joinv1.BoundKeypairRotationResponse {
+ return &joinv1.BoundKeypairRotationResponse{
+ PublicKey: msg.PublicKey,
+ }
+}
+
+func boundKeypairResultToMessage(req *joinv1.BoundKeypairResult) *messages.BoundKeypairResult {
+ if req == nil {
+ return nil
+ }
+ return &messages.BoundKeypairResult{
+ JoinState: req.JoinState,
+ PublicKey: req.PublicKey,
+ }
+}
+
+func boundKeypairResultFromMessage(msg *messages.BoundKeypairResult) *joinv1.BoundKeypairResult {
+ if msg == nil {
+ return nil
+ }
+ return &joinv1.BoundKeypairResult{
+ JoinState: msg.JoinState,
+ PublicKey: msg.PublicKey,
+ }
+}
diff --git a/lib/join/server.go b/lib/join/server.go
index 6e1ba91da23c1..94c8810b2345e 100644
--- a/lib/join/server.go
+++ b/lib/join/server.go
@@ -36,14 +36,17 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
+ "github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/keys"
+ "github.com/gravitational/teleport/lib/auth/keystore"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/events"
joinauthz "github.com/gravitational/teleport/lib/join/internal/authz"
"github.com/gravitational/teleport/lib/join/internal/diagnostic"
"github.com/gravitational/teleport/lib/join/internal/messages"
+ "github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/utils/hostid"
logutils "github.com/gravitational/teleport/lib/utils/log"
)
@@ -58,6 +61,14 @@ type AuthService interface {
GenerateBotCertsForJoin(ctx context.Context, provisionToken types.ProvisionToken, req *BotCertsParams) (*proto.Certs, string, error)
EmitAuditEvent(ctx context.Context, e apievents.AuditEvent) error
GetAuthPreference(ctx context.Context) (types.AuthPreference, error)
+ GetReadOnlyAuthPreference(context.Context) (readonly.AuthPreference, error)
+ GetClusterName(context.Context) (types.ClusterName, error)
+ GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error)
+ GetKeyStore() *keystore.Manager
+ PatchToken(context.Context, string, func(types.ProvisionToken) (types.ProvisionToken, error)) (types.ProvisionToken, error)
+ UpsertLock(context.Context, types.Lock) error
+ CheckLockInForce(constants.LockingMode, []types.LockTarget) error
+ GetClock() clockwork.Clock
}
// ServerConfig holds configuration parameters for [Server].
@@ -99,7 +110,8 @@ func (s *Server) Join(stream messages.ServerStream) (err error) {
diag := stream.Diagnostic()
defer func() {
if err != nil {
- s.handleJoinFailure(ctx, diag, err)
+ diag.Set(func(i *diagnostic.Info) { i.Error = err })
+ handleJoinFailure(ctx, s.cfg.AuthService, diag)
}
}()
@@ -187,6 +199,8 @@ func (s *Server) handleJoinMethod(
switch joinMethod {
case types.JoinMethodToken:
return s.handleTokenJoin(stream, authCtx, clientInit, provisionToken)
+ case types.JoinMethodBoundKeypair:
+ return s.handleBoundKeypairJoin(stream, authCtx, clientInit, provisionToken)
default:
// TODO(nklaassen): implement checks for all join methods.
return nil, trace.NotImplemented("join method %s is not yet implemented by the new join service", joinMethod)
@@ -312,13 +326,15 @@ func (s *Server) makeResult(
authCtx *joinauthz.Context,
clientInit *messages.ClientInit,
clientParams *messages.ClientParams,
+ rawClaims any,
provisionToken types.ProvisionToken,
) (messages.Response, error) {
switch types.SystemRole(clientInit.SystemRole) {
case types.RoleInstance:
return s.makeHostResult(ctx, diag, authCtx, clientParams.HostParams, provisionToken)
case types.RoleBot:
- return s.makeBotResult(ctx, diag, authCtx, clientParams.BotParams, provisionToken)
+ result, _, err := s.makeBotResult(ctx, diag, authCtx, clientParams.BotParams, rawClaims, provisionToken)
+ return result, trace.Wrap(err)
default:
return nil, trace.NotImplemented("new join service only supports Instance and Bot system roles, client requested %s", clientInit.SystemRole)
}
@@ -408,23 +424,24 @@ func (s *Server) makeBotResult(
diag *diagnostic.Diagnostic,
authCtx *joinauthz.Context,
botParams *messages.BotParams,
+ rawClaims any,
provisionToken types.ProvisionToken,
-) (*messages.BotResult, error) {
- certsParams, err := makeBotCertsParams(diag, authCtx, botParams)
+) (*messages.BotResult, string, error) {
+ certsParams, err := makeBotCertsParams(diag, authCtx, botParams, rawClaims)
if err != nil {
- return nil, trace.Wrap(err)
+ return nil, "", trace.Wrap(err)
}
- certs, _, err := s.cfg.AuthService.GenerateBotCertsForJoin(ctx, provisionToken, certsParams)
+ certs, botInstanceID, err := s.cfg.AuthService.GenerateBotCertsForJoin(ctx, provisionToken, certsParams)
if err != nil {
- return nil, trace.Wrap(err)
+ return nil, "", trace.Wrap(err)
}
certificates, err := convertCerts(certs)
if err != nil {
- return nil, trace.Wrap(err)
+ return nil, "", trace.Wrap(err)
}
return &messages.BotResult{
Certificates: *certificates,
- }, nil
+ }, botInstanceID, nil
}
// makeBotCertsParams returns [BotCertsParams] populated by the
@@ -433,6 +450,7 @@ func makeBotCertsParams(
diag *diagnostic.Diagnostic,
authCtx *joinauthz.Context,
botParams *messages.BotParams,
+ rawClaims any,
) (*BotCertsParams, error) {
// GenerateBotCertsForJoin requires the TLS key to be PEM-encoded.
tlsPub, err := x509.ParsePKIXPublicKey(botParams.PublicKeys.PublicTLSKey)
@@ -458,6 +476,7 @@ func makeBotCertsParams(
BotGeneration: int32(authCtx.BotGeneration),
Expires: botParams.Expires,
RemoteAddr: diag.Get().RemoteAddr,
+ RawJoinClaims: rawClaims,
}, nil
}
@@ -531,10 +550,9 @@ func setDiagnosticClientParams(diag *diagnostic.Diagnostic, clientParams *messag
}
}
-func (s *Server) handleJoinFailure(ctx context.Context, diag *diagnostic.Diagnostic, err error) {
- diag.Set(func(i *diagnostic.Info) { i.Error = err })
+func handleJoinFailure(ctx context.Context, emitter apievents.Emitter, diag *diagnostic.Diagnostic) {
log.LogAttrs(ctx, slog.LevelWarn, "Failure to join cluster occurred", diag.SlogAttrs()...)
- if err := s.cfg.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), makeAuditEvent(diag)); err != nil {
+ if err := emitter.EmitAuditEvent(context.WithoutCancel(ctx), makeAuditEvent(diag)); err != nil {
log.WarnContext(ctx, "Failed to emit failed join event", "error", err)
}
}
diff --git a/lib/join/server_boundkeypair.go b/lib/join/server_boundkeypair.go
new file mode 100644
index 0000000000000..aa9dfaa434ba1
--- /dev/null
+++ b/lib/join/server_boundkeypair.go
@@ -0,0 +1,284 @@
+// Teleport
+// Copyright (C) 2025 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 .
+
+package join
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport/api/client"
+ "github.com/gravitational/teleport/api/client/proto"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/join/boundkeypair"
+ "github.com/gravitational/teleport/lib/join/internal/authz"
+ "github.com/gravitational/teleport/lib/join/internal/diagnostic"
+ "github.com/gravitational/teleport/lib/join/internal/messages"
+)
+
+// handleBoundKeypairJoin handles join attempts for the bound keypair join
+// method.
+//
+// The bound keypair join method involves the following messages:
+//
+// client->server ClientInit
+// client<-server ServerInit
+// client->server BoundKeypairInit
+// client<-server BoundKeypairChallenge
+// client->server BoundKeypairChallengeSolution
+//
+// (optional additional steps if keypair rotation is required)
+// client<-server: BoundKeypairRotationRequest
+// client->server: BoundKeypairRotationResponse
+// client<-server: BoundKeypairChallenge
+// client->server: BoundKeypairChallengeSolution
+//
+// client<-server: Result containing BoundKeypairResult
+//
+// At this point the ServerInit message has already been sent, what's left is
+// to receive the BoundKeypairInit message, handle the challenge-response (and
+// rotation if necessary), and send the final result if everything checks out.
+func (s *Server) handleBoundKeypairJoin(
+ stream messages.ServerStream,
+ authCtx *authz.Context,
+ clientInit *messages.ClientInit,
+ provisionToken types.ProvisionToken,
+) (*messages.BotResult, error) {
+ ctx := stream.Context()
+ diag := stream.Diagnostic()
+ // Only bot joining is supported at the moment - unique ID verification is
+ // required and this is currently only implemented for bots.
+ if clientInit.SystemRole != types.RoleBot.String() {
+ return nil, trace.BadParameter("bound keypair joining is only supported for bots")
+ }
+ boundKeypairInit, err := messages.RecvRequest[*messages.BoundKeypairInit](stream)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ issueChallenge := func(challenge *messages.BoundKeypairChallenge) (*messages.BoundKeypairChallengeSolution, error) {
+ if err := stream.Send(challenge); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ solution, err := messages.RecvRequest[*messages.BoundKeypairChallengeSolution](stream)
+ return solution, trace.Wrap(err)
+ }
+ issueRotationRequest := func(rotationReq *messages.BoundKeypairRotationRequest) (*messages.BoundKeypairRotationResponse, error) {
+ if err := stream.Send(rotationReq); err != nil {
+ return nil, trace.Wrap(err)
+ }
+ rotationResp, err := messages.RecvRequest[*messages.BoundKeypairRotationResponse](stream)
+ return rotationResp, trace.Wrap(err)
+ }
+ generateBotCerts := func(ctx context.Context, previousBotInstanceID string, claims any) (*messages.Certificates, string, error) {
+ botCertsParams, err := makeBotCertsParams(diag, authCtx, boundKeypairInit.ClientParams.BotParams, claims)
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ botCertsParams.PreviousBotInstanceID = previousBotInstanceID
+ protoCerts, botInstanceID, err := s.cfg.AuthService.GenerateBotCertsForJoin(ctx, provisionToken, botCertsParams)
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ botCerts, err := convertCerts(protoCerts)
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ return botCerts, botInstanceID, nil
+ }
+ return boundkeypair.HandleBoundKeypairJoin(ctx, &boundkeypair.JoinParams{
+ AuthService: s.cfg.AuthService,
+ AuthCtx: authCtx,
+ Diag: diag,
+ ProvisionToken: provisionToken,
+ ClientInit: clientInit,
+ BoundKeypairInit: boundKeypairInit,
+ IssueChallenge: issueChallenge,
+ IssueRotationRequest: issueRotationRequest,
+ GenerateBotCerts: generateBotCerts,
+ Clock: s.clock,
+ Logger: log,
+ })
+}
+
+// AdaptRegisterUsingBoundKeypairMethod handles requests from the legacy join
+// gRPC service and adapts the request types to the protocol-agnostic types
+// defined in [messages] before calling [boundkeypair.HandleBoundKeypairJoin]
+// which contains the actual logic for bound keypair joining.
+//
+// TODO(nklaassen): DELETE IN 20 when removing the legacy join service.
+func AdaptRegisterUsingBoundKeypairMethod(
+ ctx context.Context,
+ a AuthService,
+ createBoundKeypairValidator boundkeypair.CreateBoundKeypairValidator,
+ req *proto.RegisterUsingBoundKeypairInitialRequest,
+ challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
+) (_ *client.BoundKeypairRegistrationResponse, err error) {
+ diag := diagnostic.New()
+ diag.Set(func(i *diagnostic.Info) {
+ i.RemoteAddr = req.JoinRequest.RemoteAddr
+ i.Role = req.JoinRequest.Role.String()
+ i.RequestedJoinMethod = string(types.JoinMethodBoundKeypair)
+ i.BotInstanceID = req.JoinRequest.BotInstanceID
+ i.BotGeneration = uint64(req.JoinRequest.BotGeneration)
+ })
+ defer func() {
+ if err != nil {
+ diag.Set(func(i *diagnostic.Info) { i.Error = err })
+ handleJoinFailure(ctx, a, diag)
+ }
+ }()
+
+ // Construct an [authz.Context] to pass to HandleBoundKeypairJoin.
+ authCtx := &authz.Context{
+ // These are verified at the gRPC layer by the legacy join service.
+ BotInstanceID: req.JoinRequest.BotInstanceID,
+ BotGeneration: uint64(req.JoinRequest.BotGeneration),
+ }
+
+ // Only bot joining is supported at the moment - unique ID verification is
+ // required and this is currently only implemented for bots.
+ if req.JoinRequest.Role != types.RoleBot {
+ return nil, trace.BadParameter("bound keypair joining is only supported for bots")
+ }
+
+ provisionToken, err := a.ValidateToken(ctx, req.JoinRequest.Token)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ // Set any diagnostic info we can get from the token.
+ diag.Set(func(i *diagnostic.Info) {
+ i.SafeTokenName = provisionToken.GetSafeName()
+ i.TokenJoinMethod = string(provisionToken.GetJoinMethod())
+ i.TokenExpires = provisionToken.Expiry()
+ i.BotName = provisionToken.GetBotName()
+ })
+ if provisionToken.GetJoinMethod() != types.JoinMethodBoundKeypair {
+ return nil, trace.BadParameter("specified join token is not for `%s` method", types.JoinMethodBoundKeypair)
+ }
+
+ // Assert that the provision token allows the requested system role.
+ if err := ProvisionTokenAllowsRole(provisionToken, req.JoinRequest.Role); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ clientInit := clientInitFromRegisterUsingTokenRequest(req.JoinRequest, string(types.JoinMethodBoundKeypair))
+ clientParams, err := clientParamsFromRegisterUsingTokenRequest(req.JoinRequest)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ boundKeypairInit := &messages.BoundKeypairInit{
+ ClientParams: *clientParams,
+ InitialJoinSecret: req.InitialJoinSecret,
+ PreviousJoinState: req.PreviousJoinState,
+ }
+
+ generateBotCerts := func(ctx context.Context, previousBotInstanceID string, claims any) (*messages.Certificates, string, error) {
+ botCertsParams, err := makeBotCertsParams(diag, authCtx, clientParams.BotParams, claims)
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ botCertsParams.PreviousBotInstanceID = previousBotInstanceID
+ protoCerts, botInstanceID, err := a.GenerateBotCertsForJoin(ctx, provisionToken, botCertsParams)
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ botCerts, err := convertCerts(protoCerts)
+ if err != nil {
+ return nil, "", trace.Wrap(err)
+ }
+ return botCerts, botInstanceID, nil
+ }
+
+ botResult, err := boundkeypair.HandleBoundKeypairJoin(ctx, &boundkeypair.JoinParams{
+ AuthService: a,
+ AuthCtx: authCtx,
+ Diag: diag,
+ ProvisionToken: provisionToken,
+ ClientInit: clientInit,
+ BoundKeypairInit: boundKeypairInit,
+ IssueChallenge: adaptBoundKeypairChallenge(challengeResponse),
+ IssueRotationRequest: adaptBoundKeypairRotationRequest(challengeResponse),
+ CreateBoundKeypairValidator: createBoundKeypairValidator,
+ GenerateBotCerts: generateBotCerts,
+ Clock: a.GetClock(),
+ Logger: log,
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ certs, err := protoCertsFromCertificates(botResult.Certificates)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ return &client.BoundKeypairRegistrationResponse{
+ Certs: certs,
+ BoundPublicKey: string(botResult.BoundKeypairResult.PublicKey),
+ JoinState: botResult.BoundKeypairResult.JoinState,
+ }, nil
+}
+
+func adaptBoundKeypairChallenge(challengeResponseFunc client.RegisterUsingBoundKeypairChallengeResponseFunc) boundkeypair.ChallengeResponseFunc {
+ return func(challengeMsg *messages.BoundKeypairChallenge) (*messages.BoundKeypairChallengeSolution, error) {
+ challenge := &proto.RegisterUsingBoundKeypairMethodResponse{
+ Response: &proto.RegisterUsingBoundKeypairMethodResponse_Challenge{
+ Challenge: &proto.RegisterUsingBoundKeypairChallenge{
+ PublicKey: string(challengeMsg.PublicKey),
+ Challenge: challengeMsg.Challenge,
+ },
+ },
+ }
+ resp, err := challengeResponseFunc(challenge)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ challengeResp := resp.GetChallengeResponse()
+ if challengeResp == nil {
+ return nil, trace.BadParameter("client did not send challenge response, got %T", resp.Payload)
+ }
+ return &messages.BoundKeypairChallengeSolution{
+ Solution: challengeResp.Solution,
+ }, nil
+ }
+}
+
+func adaptBoundKeypairRotationRequest(challengeResponseFunc client.RegisterUsingBoundKeypairChallengeResponseFunc) boundkeypair.RotationFunc {
+ return func(rotationMsg *messages.BoundKeypairRotationRequest) (*messages.BoundKeypairRotationResponse, error) {
+ suite, err := types.SignatureAlgorithmSuiteFromString(rotationMsg.SignatureAlgorithmSuite)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ challenge := &proto.RegisterUsingBoundKeypairMethodResponse{
+ Response: &proto.RegisterUsingBoundKeypairMethodResponse_Rotation{
+ Rotation: &proto.RegisterUsingBoundKeypairRotationRequest{
+ SignatureAlgorithmSuite: suite,
+ },
+ },
+ }
+ resp, err := challengeResponseFunc(challenge)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ rotationResp := resp.GetRotationResponse()
+ if rotationResp == nil {
+ return nil, trace.BadParameter("client did not send rotation response, got %T", resp.Payload)
+ }
+ return &messages.BoundKeypairRotationResponse{
+ PublicKey: []byte(rotationResp.PublicKey),
+ }, nil
+ }
+}
diff --git a/lib/join/server_token.go b/lib/join/server_token.go
index e58e711b6515e..54688bfba0779 100644
--- a/lib/join/server_token.go
+++ b/lib/join/server_token.go
@@ -47,6 +47,7 @@ func (s *Server) handleTokenJoin(
authCtx,
clientInit,
&tokenInit.ClientParams,
+ nil, /*rawClaims*/
provisionToken,
)
return result, trace.Wrap(err)