diff --git a/api/client/proto/joinservice.pb.go b/api/client/proto/joinservice.pb.go
index f52c07e44d9..0dadaf7ecfc 100644
--- a/api/client/proto/joinservice.pb.go
+++ b/api/client/proto/joinservice.pb.go
@@ -1443,9 +1443,11 @@ func (m *RegisterUsingBoundKeypairCertificates) GetPublicKey() string {
// RegisterUsingBoundKeypairRotationRequest is the response sent by the server
// when a keypair rotation is required.
type RegisterUsingBoundKeypairRotationRequest struct {
- XXX_NoUnkeyedLiteral struct{} `json:"-"`
- XXX_unrecognized []byte `json:"-"`
- XXX_sizecache int32 `json:"-"`
+ // The signature algorithm suite in use by the cluster.
+ SignatureAlgorithmSuite types.SignatureAlgorithmSuite `protobuf:"varint,1,opt,name=signature_algorithm_suite,json=signatureAlgorithmSuite,proto3,enum=types.SignatureAlgorithmSuite" json:"signature_algorithm_suite,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
}
func (m *RegisterUsingBoundKeypairRotationRequest) Reset() {
@@ -1483,6 +1485,13 @@ func (m *RegisterUsingBoundKeypairRotationRequest) XXX_DiscardUnknown() {
var xxx_messageInfo_RegisterUsingBoundKeypairRotationRequest proto.InternalMessageInfo
+func (m *RegisterUsingBoundKeypairRotationRequest) GetSignatureAlgorithmSuite() types.SignatureAlgorithmSuite {
+ if m != nil {
+ return m.SignatureAlgorithmSuite
+ }
+ return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED
+}
+
// RegisterUsingBoundKeypairMethodResponse is a response sent by the server
// during the bound-keypair joining process. Multiple requests and responses are
// expected to be exchanged during one join process.
@@ -1623,86 +1632,88 @@ func init() {
}
var fileDescriptor_d7e760ce923b836e = []byte{
- // 1254 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x4b, 0x6f, 0xdb, 0xc6,
- 0x13, 0x17, 0xe5, 0xd8, 0x89, 0x46, 0xca, 0x43, 0x9b, 0x20, 0x7f, 0x59, 0x88, 0x5f, 0xfc, 0x3b,
- 0xb1, 0x9c, 0x36, 0x92, 0xab, 0x5e, 0x8a, 0x9c, 0xea, 0x17, 0x20, 0xc7, 0x75, 0x6b, 0x30, 0x29,
- 0x8a, 0xf6, 0x42, 0xac, 0xa8, 0xad, 0xbc, 0x11, 0x43, 0xb2, 0xbb, 0x2b, 0x03, 0xea, 0xb9, 0x97,
- 0x7e, 0x80, 0x9c, 0xfb, 0x09, 0x0a, 0xf4, 0xd0, 0x73, 0xcf, 0x3d, 0x14, 0x45, 0x6f, 0xbd, 0x16,
- 0xfe, 0x0e, 0xbd, 0x17, 0xdc, 0x5d, 0x4a, 0x24, 0x45, 0x4a, 0x32, 0x90, 0xf6, 0x62, 0x99, 0x33,
- 0xb3, 0xbf, 0x79, 0xcf, 0x2c, 0x09, 0x4d, 0x41, 0x5c, 0x12, 0xf8, 0x4c, 0xb4, 0x5c, 0xd2, 0xc7,
- 0xce, 0xa8, 0xe5, 0xb8, 0x94, 0x78, 0xa2, 0x15, 0x30, 0x5f, 0xf8, 0xad, 0xd7, 0x3e, 0xf5, 0x38,
- 0x61, 0x97, 0xd4, 0x21, 0x4d, 0x49, 0x41, 0xcb, 0xf2, 0xa7, 0xde, 0x98, 0x79, 0xcc, 0x21, 0x4c,
- 0x70, 0x75, 0xa0, 0xbe, 0x95, 0x96, 0x14, 0xa3, 0x80, 0x70, 0xf5, 0x57, 0x89, 0x98, 0x3f, 0x1b,
- 0xb0, 0x66, 0x91, 0x3e, 0xe5, 0x82, 0xb0, 0xcf, 0x39, 0xf5, 0xfa, 0x27, 0xfb, 0x67, 0x67, 0x44,
- 0x5c, 0xf8, 0x3d, 0x8b, 0x7c, 0x33, 0x24, 0x5c, 0x20, 0x0c, 0x8f, 0x98, 0x16, 0xb0, 0x87, 0xa1,
- 0x84, 0x2d, 0xfc, 0x01, 0xf1, 0x6c, 0xa6, 0xf8, 0x35, 0x63, 0xd3, 0x68, 0x94, 0xdb, 0x9b, 0x4d,
- 0x85, 0x9a, 0xc0, 0x7a, 0x15, 0x0a, 0x6a, 0x1c, 0x6b, 0x95, 0xe5, 0xb1, 0xd0, 0x1e, 0x3c, 0xe0,
- 0x82, 0xdb, 0xb4, 0x47, 0x3c, 0x41, 0xc5, 0x68, 0x0c, 0x5d, 0xdc, 0x34, 0x1a, 0x15, 0x0b, 0x71,
- 0xc1, 0x4f, 0x34, 0x4b, 0x9f, 0x30, 0xbb, 0xb0, 0x9e, 0x67, 0x35, 0x0f, 0x7c, 0x8f, 0x13, 0xf4,
- 0x08, 0x4a, 0xce, 0x05, 0x76, 0x5d, 0xe2, 0xf5, 0x89, 0xb4, 0xb1, 0x64, 0x4d, 0x08, 0xc8, 0x84,
- 0x65, 0x19, 0x28, 0xa9, 0xa2, 0xdc, 0xae, 0xa8, 0x68, 0x34, 0x0f, 0x43, 0x9a, 0xa5, 0x58, 0xe6,
- 0x6f, 0x06, 0x6c, 0x24, 0x94, 0xec, 0x7f, 0x3b, 0x64, 0xe4, 0x3f, 0x0f, 0xce, 0xff, 0xe1, 0x36,
- 0x16, 0x82, 0x70, 0x41, 0x7a, 0x76, 0x0f, 0x0b, 0xac, 0xa3, 0x52, 0x89, 0x88, 0x47, 0x58, 0x60,
- 0xb4, 0x05, 0x15, 0xec, 0x38, 0x84, 0x73, 0xa5, 0xbf, 0xb6, 0x24, 0x1d, 0x2e, 0x2b, 0x9a, 0x84,
- 0x33, 0x7b, 0xb0, 0x99, 0xef, 0xcd, 0x3b, 0x0b, 0xda, 0x31, 0xec, 0x24, 0xbd, 0x3c, 0xd7, 0x89,
- 0x39, 0x8c, 0x60, 0xc6, 0xca, 0xea, 0x70, 0x8b, 0xfb, 0xee, 0x50, 0x50, 0xdf, 0x93, 0xba, 0x2a,
- 0xd6, 0xf8, 0xd9, 0xfc, 0xdb, 0x80, 0xed, 0x6c, 0x9c, 0x13, 0x8f, 0x0a, 0x8a, 0xdd, 0x28, 0x3a,
- 0x87, 0x50, 0x09, 0x1b, 0xe5, 0xda, 0x01, 0x2f, 0x87, 0xa7, 0x22, 0x90, 0x55, 0xb8, 0x49, 0x06,
- 0x76, 0xe8, 0x80, 0x0a, 0x6e, 0xa7, 0x60, 0xad, 0x90, 0x41, 0xe8, 0x17, 0xfa, 0x1f, 0xac, 0x90,
- 0x81, 0x3d, 0x20, 0x23, 0x19, 0xd2, 0x90, 0xb3, 0x4c, 0x06, 0xa7, 0x64, 0x84, 0x3e, 0x05, 0xa4,
- 0x32, 0x80, 0x43, 0x83, 0xed, 0x00, 0x33, 0xfc, 0x86, 0xd7, 0x6e, 0x48, 0xf5, 0x1b, 0x3a, 0x32,
- 0xaf, 0xce, 0xcf, 0xf6, 0x27, 0x32, 0xe7, 0xa1, 0x08, 0x11, 0x84, 0x71, 0xab, 0x8a, 0x53, 0x64,
- 0x7e, 0x70, 0x03, 0x8a, 0x64, 0x60, 0xfe, 0x9e, 0x6e, 0xc7, 0xb1, 0xdf, 0x91, 0xad, 0xfb, 0x70,
- 0x83, 0x7a, 0x34, 0x72, 0xf4, 0x3d, 0xad, 0x69, 0x91, 0x58, 0x75, 0x0a, 0x96, 0x3c, 0x8a, 0x6c,
- 0x40, 0xe3, 0xa4, 0xda, 0x4c, 0xa7, 0x43, 0x27, 0xb5, 0x39, 0x13, 0x70, 0x2a, 0x89, 0x9d, 0x82,
- 0x55, 0x75, 0xd2, 0xc4, 0x83, 0x12, 0xdc, 0x0c, 0xf0, 0xc8, 0xf5, 0x71, 0xcf, 0xfc, 0xc1, 0x48,
- 0x75, 0x6a, 0xcc, 0x21, 0x5d, 0x07, 0x9f, 0x40, 0x35, 0x6e, 0x4e, 0x3c, 0x8f, 0x6b, 0x93, 0x40,
- 0x1e, 0x7b, 0x0e, 0x1b, 0x05, 0x82, 0xf4, 0x0e, 0x19, 0x91, 0xc3, 0x00, 0xbb, 0x9d, 0x82, 0x75,
- 0x2f, 0xa6, 0x5c, 0xc5, 0x67, 0x7b, 0x46, 0x91, 0x86, 0xd9, 0x93, 0xcc, 0xb8, 0x85, 0x3f, 0x1a,
- 0x50, 0xcb, 0x4b, 0x14, 0x7a, 0x08, 0x2b, 0xc1, 0xb0, 0xeb, 0x52, 0x47, 0x57, 0xa8, 0x7e, 0x42,
- 0x1b, 0x50, 0x76, 0x18, 0xc1, 0x82, 0xc4, 0x5b, 0x12, 0x14, 0x49, 0x36, 0xe4, 0x33, 0x40, 0x5a,
- 0x20, 0x96, 0x6a, 0x55, 0x43, 0x56, 0x55, 0x71, 0x62, 0x1a, 0xd1, 0x2e, 0xdc, 0xd3, 0xe2, 0x9c,
- 0xf6, 0x3d, 0x2c, 0x86, 0x8c, 0xc8, 0x5a, 0xaa, 0x58, 0x77, 0x15, 0xfd, 0x65, 0x44, 0x36, 0xbf,
- 0x84, 0x87, 0xd9, 0xe1, 0x40, 0x3b, 0x10, 0x0a, 0xeb, 0x27, 0xbb, 0xeb, 0xfa, 0x5d, 0x6d, 0xf5,
- 0x9d, 0x09, 0xf9, 0xc0, 0xf5, 0xbb, 0xa1, 0x57, 0x9c, 0x38, 0x8c, 0x44, 0x13, 0x56, 0x3f, 0x99,
- 0x3f, 0x15, 0xe1, 0xfe, 0x67, 0x0c, 0x3b, 0xae, 0x54, 0x47, 0x62, 0x35, 0x77, 0xf3, 0x82, 0xe0,
- 0x1e, 0x61, 0xbc, 0x66, 0x6c, 0x2e, 0x35, 0xca, 0xed, 0x1d, 0x1d, 0xd5, 0x0c, 0xe1, 0x66, 0x47,
- 0x49, 0x1e, 0x7b, 0x82, 0x8d, 0xac, 0xe8, 0x1c, 0xfa, 0x02, 0xee, 0xea, 0x80, 0xdb, 0x11, 0x54,
- 0x51, 0x42, 0x35, 0x67, 0x40, 0x9d, 0xab, 0x13, 0x09, 0xc4, 0x3b, 0x41, 0x82, 0x58, 0x7f, 0x0e,
- 0x95, 0x38, 0x1f, 0xdd, 0x83, 0xa5, 0xb0, 0x5b, 0xd5, 0xf0, 0x0a, 0xff, 0x45, 0x0f, 0x60, 0xf9,
- 0x12, 0xbb, 0x43, 0x55, 0xe1, 0x25, 0x4b, 0x3d, 0x3c, 0x2f, 0x7e, 0x64, 0xd4, 0xf7, 0xe1, 0x7e,
- 0x86, 0x8a, 0xeb, 0x40, 0x98, 0x7f, 0x1a, 0xa9, 0xb1, 0xaa, 0xfc, 0x48, 0xf6, 0xac, 0xf3, 0x6e,
- 0xb6, 0x44, 0xa7, 0x30, 0x6b, 0x4f, 0x1c, 0xc2, 0x1d, 0x5f, 0xea, 0x4e, 0xac, 0xcf, 0x72, 0xbb,
- 0x9e, 0x1f, 0xe0, 0x4e, 0xc1, 0xba, 0xad, 0xce, 0x68, 0x42, 0xd8, 0x17, 0xfa, 0xb4, 0x39, 0x84,
- 0xad, 0x19, 0x8e, 0xe9, 0xde, 0x5d, 0x9f, 0x5a, 0x18, 0x9d, 0x42, 0x7c, 0x65, 0x2c, 0xd6, 0x8d,
- 0x00, 0xb7, 0xa2, 0x31, 0x14, 0x4e, 0xc0, 0xe4, 0x06, 0x39, 0xf0, 0x87, 0x5e, 0xef, 0x94, 0x8c,
- 0x02, 0x4c, 0xd9, 0xbf, 0x31, 0xfc, 0x9b, 0x70, 0x9f, 0x2a, 0x58, 0x5b, 0x82, 0xc5, 0x3a, 0xa3,
- 0x64, 0x55, 0x35, 0xeb, 0x85, 0x4f, 0xbd, 0x97, 0x92, 0x11, 0xca, 0x07, 0x8c, 0x5c, 0x52, 0x7f,
- 0xc8, 0xf5, 0x01, 0x81, 0x05, 0x89, 0x5a, 0x3b, 0x62, 0xc9, 0x03, 0x21, 0xc3, 0xec, 0xc0, 0xd3,
- 0x5c, 0x7f, 0xae, 0xb7, 0x14, 0x5f, 0xc0, 0x6e, 0x2e, 0x92, 0xe5, 0xab, 0x49, 0x32, 0x06, 0x5a,
- 0x03, 0x50, 0xb3, 0xca, 0x9e, 0xd4, 0x72, 0x49, 0x51, 0x4e, 0xc9, 0xc8, 0xfc, 0xa5, 0x08, 0x4f,
- 0x72, 0xc1, 0x92, 0xd5, 0x7b, 0x94, 0xd8, 0x38, 0x99, 0x0b, 0x22, 0x3f, 0x47, 0xe3, 0xa5, 0xd3,
- 0x9d, 0xb1, 0x74, 0x3e, 0x98, 0x87, 0xb9, 0xd8, 0xde, 0x41, 0x36, 0x54, 0x99, 0x8e, 0xc3, 0x44,
- 0xc5, 0x92, 0x54, 0xb1, 0x37, 0x4f, 0x45, 0x3a, 0x80, 0xe1, 0x72, 0x61, 0x29, 0x5a, 0x7c, 0x6d,
- 0x60, 0x30, 0xe7, 0x9b, 0x3b, 0x27, 0x0b, 0xc9, 0xfb, 0x56, 0x31, 0x75, 0xdf, 0x32, 0xbf, 0x37,
- 0xe0, 0x71, 0xbe, 0x0e, 0xc2, 0x04, 0xfd, 0x9a, 0x3a, 0x58, 0x10, 0x3e, 0xb9, 0x99, 0x19, 0xb9,
- 0x37, 0xb3, 0xd0, 0x94, 0x58, 0xb9, 0xaa, 0xc1, 0x5f, 0x7a, 0x1d, 0x95, 0x69, 0xca, 0xd2, 0xa5,
- 0x74, 0xbd, 0x3c, 0x85, 0xc6, 0x02, 0xa1, 0x53, 0x93, 0xe3, 0x6d, 0x71, 0x46, 0x0b, 0xa7, 0x06,
- 0xc8, 0x49, 0x7a, 0x80, 0x94, 0xdb, 0xbb, 0x0b, 0x57, 0x43, 0x72, 0xd6, 0x1c, 0x25, 0x67, 0xcd,
- 0xfb, 0x73, 0x61, 0x62, 0x11, 0x1c, 0xcf, 0x22, 0x74, 0x06, 0xb7, 0xa2, 0xb4, 0xeb, 0xd2, 0x69,
- 0x2d, 0x5e, 0x3a, 0x51, 0xc9, 0x8f, 0x21, 0xe2, 0xa3, 0xad, 0xfd, 0x76, 0x19, 0xca, 0x6a, 0x90,
- 0xc8, 0xb7, 0x3a, 0x44, 0xe1, 0x61, 0xf6, 0x4b, 0x0c, 0xda, 0xce, 0x52, 0x99, 0x7e, 0x33, 0xab,
- 0x3f, 0x9e, 0x23, 0xa5, 0xd4, 0x36, 0x8c, 0x3d, 0x03, 0xf9, 0x50, 0xcb, 0xbb, 0xfc, 0xa3, 0x27,
- 0x59, 0x30, 0xd3, 0xef, 0x3a, 0xf5, 0x9d, 0xb9, 0x72, 0x31, 0x85, 0x69, 0xdf, 0xc6, 0xd7, 0xbe,
- 0x6c, 0xdf, 0xd2, 0xd7, 0xdc, 0x6c, 0xdf, 0xa6, 0xee, 0x8e, 0x52, 0x15, 0x83, 0xd5, 0xdc, 0x45,
- 0x85, 0x32, 0x8d, 0xce, 0xd8, 0xd1, 0xf5, 0xc6, 0x7c, 0xc1, 0x98, 0xce, 0xef, 0xd2, 0xef, 0x86,
- 0xd3, 0x25, 0x8e, 0x9e, 0xcd, 0xab, 0x9b, 0xa4, 0x01, 0xcd, 0x45, 0xc5, 0x63, 0x66, 0x1c, 0x01,
- 0x9a, 0xde, 0x72, 0x68, 0xee, 0x02, 0xac, 0x27, 0x06, 0xc4, 0xc1, 0xc7, 0xbf, 0x5e, 0xad, 0x1b,
- 0x7f, 0x5c, 0xad, 0x1b, 0x7f, 0x5d, 0xad, 0x1b, 0x5f, 0xb5, 0xfb, 0x54, 0x5c, 0x0c, 0xbb, 0x4d,
- 0xc7, 0x7f, 0xd3, 0xea, 0x33, 0x7c, 0x49, 0x55, 0x25, 0x63, 0xb7, 0x35, 0xfe, 0x9a, 0x80, 0x03,
- 0x9a, 0xf8, 0xe8, 0xd0, 0x5d, 0x91, 0x3f, 0x1f, 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x72, 0x17,
- 0x9a, 0x35, 0xd2, 0x10, 0x00, 0x00,
+ // 1294 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xcf, 0x8f, 0xd3, 0xc6,
+ 0x17, 0x8f, 0xb3, 0xec, 0x42, 0x5e, 0xc2, 0xc2, 0x0e, 0x08, 0x76, 0x23, 0x08, 0x8b, 0xbf, 0xc0,
+ 0x86, 0x6f, 0x4b, 0x42, 0xd3, 0x4b, 0xc5, 0xa9, 0xd9, 0x5d, 0xa4, 0x2c, 0x74, 0xdb, 0x95, 0xa1,
+ 0xaa, 0xca, 0xc5, 0x9a, 0x38, 0xd3, 0xec, 0x10, 0x63, 0xbb, 0x33, 0xe3, 0x95, 0xd2, 0x73, 0x55,
+ 0xa9, 0x7f, 0x00, 0xe7, 0xfe, 0x05, 0x95, 0x7a, 0xe8, 0xb9, 0xe7, 0x1e, 0xaa, 0xaa, 0xb7, 0x5e,
+ 0x2b, 0xfe, 0x87, 0xde, 0x2b, 0xcf, 0x8c, 0x1d, 0xdb, 0xb1, 0x93, 0x20, 0xd1, 0x5e, 0x58, 0xfc,
+ 0xde, 0x9b, 0xcf, 0xfb, 0xfd, 0xde, 0x4c, 0xa0, 0x23, 0x88, 0x4b, 0x02, 0x9f, 0x89, 0xae, 0x4b,
+ 0xc6, 0xd8, 0x99, 0x76, 0x1d, 0x97, 0x12, 0x4f, 0x74, 0x03, 0xe6, 0x0b, 0xbf, 0xfb, 0xd2, 0xa7,
+ 0x1e, 0x27, 0xec, 0x8c, 0x3a, 0xa4, 0x23, 0x29, 0x68, 0x5d, 0xfe, 0x69, 0xb6, 0x17, 0x1e, 0x73,
+ 0x08, 0x13, 0x5c, 0x1d, 0x68, 0xde, 0xce, 0x4b, 0x8a, 0x69, 0x40, 0xb8, 0xfa, 0x57, 0x89, 0x98,
+ 0x3f, 0x1b, 0x70, 0xd3, 0x22, 0x63, 0xca, 0x05, 0x61, 0x9f, 0x73, 0xea, 0x8d, 0x8f, 0xfa, 0xc7,
+ 0xc7, 0x44, 0x9c, 0xfa, 0x23, 0x8b, 0x7c, 0x1d, 0x12, 0x2e, 0x10, 0x86, 0x1b, 0x4c, 0x0b, 0xd8,
+ 0x61, 0x24, 0x61, 0x0b, 0x7f, 0x42, 0x3c, 0x9b, 0x29, 0xfe, 0xb6, 0xb1, 0x6b, 0xb4, 0xeb, 0xbd,
+ 0xdd, 0x8e, 0x42, 0xcd, 0x60, 0x3d, 0x8f, 0x04, 0x35, 0x8e, 0xb5, 0xc3, 0xca, 0x58, 0xe8, 0x21,
+ 0x5c, 0xe5, 0x82, 0xdb, 0x74, 0x44, 0x3c, 0x41, 0xc5, 0x34, 0x81, 0xae, 0xee, 0x1a, 0xed, 0x86,
+ 0x85, 0xb8, 0xe0, 0x47, 0x9a, 0xa5, 0x4f, 0x98, 0x43, 0x68, 0x95, 0x59, 0xcd, 0x03, 0xdf, 0xe3,
+ 0x04, 0xdd, 0x80, 0x9a, 0x73, 0x8a, 0x5d, 0x97, 0x78, 0x63, 0x22, 0x6d, 0xac, 0x59, 0x33, 0x02,
+ 0x32, 0x61, 0x5d, 0x06, 0x4a, 0xaa, 0xa8, 0xf7, 0x1a, 0x2a, 0x1a, 0x9d, 0x83, 0x88, 0x66, 0x29,
+ 0x96, 0xf9, 0x9b, 0x01, 0xb7, 0x32, 0x4a, 0xfa, 0xdf, 0x84, 0x8c, 0xfc, 0xe7, 0xc1, 0xf9, 0x1f,
+ 0x5c, 0xc4, 0x42, 0x10, 0x2e, 0xc8, 0xc8, 0x1e, 0x61, 0x81, 0x75, 0x54, 0x1a, 0x31, 0xf1, 0x10,
+ 0x0b, 0x8c, 0x6e, 0x43, 0x03, 0x3b, 0x0e, 0xe1, 0x5c, 0xe9, 0xdf, 0x5e, 0x93, 0x0e, 0xd7, 0x15,
+ 0x4d, 0xc2, 0x99, 0x23, 0xd8, 0x2d, 0xf7, 0xe6, 0x9d, 0x05, 0xed, 0x31, 0xec, 0x65, 0xbd, 0x3c,
+ 0xd1, 0x89, 0x39, 0x88, 0x61, 0x12, 0x65, 0x4d, 0xb8, 0xc0, 0x7d, 0x37, 0x14, 0xd4, 0xf7, 0xa4,
+ 0xae, 0x86, 0x95, 0x7c, 0x9b, 0x7f, 0x1b, 0x70, 0xa7, 0x18, 0xe7, 0xc8, 0xa3, 0x82, 0x62, 0x37,
+ 0x8e, 0xce, 0x01, 0x34, 0xa2, 0x46, 0x79, 0xeb, 0x80, 0xd7, 0xa3, 0x53, 0x31, 0xc8, 0x0e, 0x9c,
+ 0x27, 0x13, 0x3b, 0x72, 0x40, 0x05, 0x77, 0x50, 0xb1, 0x36, 0xc8, 0x24, 0xf2, 0x0b, 0x5d, 0x87,
+ 0x0d, 0x32, 0xb1, 0x27, 0x64, 0x2a, 0x43, 0x1a, 0x71, 0xd6, 0xc9, 0xe4, 0x29, 0x99, 0xa2, 0x4f,
+ 0x01, 0xa9, 0x0c, 0xe0, 0xc8, 0x60, 0x3b, 0xc0, 0x0c, 0xbf, 0xe2, 0xdb, 0xe7, 0xa4, 0xfa, 0x5b,
+ 0x3a, 0x32, 0xcf, 0x4f, 0x8e, 0xfb, 0x33, 0x99, 0x93, 0x48, 0x84, 0x08, 0xc2, 0xb8, 0xb5, 0x85,
+ 0x73, 0x64, 0xbe, 0x7f, 0x0e, 0xaa, 0x64, 0x62, 0xfe, 0x9e, 0x6f, 0xc7, 0xc4, 0xef, 0xd8, 0xd6,
+ 0x3e, 0x9c, 0xa3, 0x1e, 0x8d, 0x1d, 0x7d, 0x4f, 0x6b, 0x5a, 0x25, 0x56, 0x83, 0x8a, 0x25, 0x8f,
+ 0x22, 0x1b, 0x50, 0x92, 0x54, 0x9b, 0xe9, 0x74, 0xe8, 0xa4, 0x76, 0x16, 0x02, 0xce, 0x25, 0x71,
+ 0x50, 0xb1, 0xb6, 0x9c, 0x3c, 0x71, 0xbf, 0x06, 0xe7, 0x03, 0x3c, 0x75, 0x7d, 0x3c, 0x32, 0x7f,
+ 0x30, 0x72, 0x9d, 0x9a, 0x72, 0x48, 0xd7, 0xc1, 0x27, 0xb0, 0x95, 0x36, 0x27, 0x9d, 0xc7, 0x9b,
+ 0xb3, 0x40, 0x3e, 0xf6, 0x1c, 0x36, 0x0d, 0x04, 0x19, 0x1d, 0x30, 0x22, 0x87, 0x01, 0x76, 0x07,
+ 0x15, 0xeb, 0x72, 0x4a, 0xb9, 0x8a, 0xcf, 0x9d, 0x05, 0x45, 0x1a, 0x65, 0x4f, 0x32, 0xd3, 0x16,
+ 0xfe, 0x68, 0xc0, 0x76, 0x59, 0xa2, 0xd0, 0x35, 0xd8, 0x08, 0xc2, 0xa1, 0x4b, 0x1d, 0x5d, 0xa1,
+ 0xfa, 0x0b, 0xdd, 0x82, 0xba, 0xc3, 0x08, 0x16, 0x24, 0xdd, 0x92, 0xa0, 0x48, 0xb2, 0x21, 0x1f,
+ 0x00, 0xd2, 0x02, 0xa9, 0x54, 0xab, 0x1a, 0xb2, 0xb6, 0x14, 0x27, 0xa5, 0x11, 0xdd, 0x87, 0xcb,
+ 0x5a, 0x9c, 0xd3, 0xb1, 0x87, 0x45, 0xc8, 0x88, 0xac, 0xa5, 0x86, 0x75, 0x49, 0xd1, 0x9f, 0xc5,
+ 0x64, 0xf3, 0x4b, 0xb8, 0x56, 0x1c, 0x0e, 0xb4, 0x07, 0x91, 0xb0, 0xfe, 0xb2, 0x87, 0xae, 0x3f,
+ 0xd4, 0x56, 0x6f, 0xce, 0xc8, 0xfb, 0xae, 0x3f, 0x8c, 0xbc, 0xe2, 0xc4, 0x61, 0x24, 0x9e, 0xb0,
+ 0xfa, 0xcb, 0xfc, 0xa9, 0x0a, 0x57, 0x3e, 0x63, 0xd8, 0x71, 0xa5, 0x3a, 0x92, 0xaa, 0xb9, 0xf3,
+ 0xa7, 0x04, 0x8f, 0x08, 0xe3, 0xdb, 0xc6, 0xee, 0x5a, 0xbb, 0xde, 0xdb, 0xd3, 0x51, 0x2d, 0x10,
+ 0xee, 0x0c, 0x94, 0xe4, 0x63, 0x4f, 0xb0, 0xa9, 0x15, 0x9f, 0x43, 0x5f, 0xc0, 0x25, 0x1d, 0x70,
+ 0x3b, 0x86, 0xaa, 0x4a, 0xa8, 0xce, 0x02, 0xa8, 0x13, 0x75, 0x22, 0x83, 0xb8, 0x19, 0x64, 0x88,
+ 0xcd, 0x47, 0xd0, 0x48, 0xf3, 0xd1, 0x65, 0x58, 0x8b, 0xba, 0x55, 0x0d, 0xaf, 0xe8, 0xbf, 0xe8,
+ 0x2a, 0xac, 0x9f, 0x61, 0x37, 0x54, 0x15, 0x5e, 0xb3, 0xd4, 0xc7, 0xa3, 0xea, 0x47, 0x46, 0xb3,
+ 0x0f, 0x57, 0x0a, 0x54, 0xbc, 0x0d, 0x84, 0xf9, 0xa7, 0x91, 0x1b, 0xab, 0xca, 0x8f, 0x6c, 0xcf,
+ 0x3a, 0xef, 0x66, 0x4b, 0x0c, 0x2a, 0x8b, 0xf6, 0xc4, 0x01, 0x6c, 0xfa, 0x52, 0x77, 0x66, 0x7d,
+ 0xd6, 0x7b, 0xcd, 0xf2, 0x00, 0x0f, 0x2a, 0xd6, 0x45, 0x75, 0x46, 0x13, 0xa2, 0xbe, 0xd0, 0xa7,
+ 0xcd, 0x10, 0x6e, 0x2f, 0x70, 0x4c, 0xf7, 0x6e, 0x6b, 0x6e, 0x61, 0x0c, 0x2a, 0xe9, 0x95, 0xb1,
+ 0x5a, 0x37, 0x02, 0x5c, 0x88, 0xc7, 0x50, 0x34, 0x01, 0xb3, 0x1b, 0x64, 0xdf, 0x0f, 0xbd, 0xd1,
+ 0x53, 0x32, 0x0d, 0x30, 0x65, 0xff, 0xc6, 0xf0, 0xef, 0xc0, 0x15, 0xaa, 0x60, 0x6d, 0x09, 0x96,
+ 0xea, 0x8c, 0x9a, 0xb5, 0xa5, 0x59, 0x4f, 0x7c, 0xea, 0x3d, 0x93, 0x8c, 0x48, 0x3e, 0x60, 0xe4,
+ 0x8c, 0xfa, 0x21, 0xd7, 0x07, 0x04, 0x16, 0x24, 0x6e, 0xed, 0x98, 0x25, 0x0f, 0x44, 0x0c, 0x73,
+ 0x00, 0xff, 0x2f, 0xf5, 0xe7, 0xed, 0x96, 0xe2, 0x13, 0xb8, 0x5f, 0x8a, 0x64, 0xf9, 0x6a, 0x92,
+ 0x24, 0x40, 0x37, 0x01, 0xd4, 0xac, 0xb2, 0x67, 0xb5, 0x5c, 0x53, 0x94, 0xa7, 0x64, 0x6a, 0xfe,
+ 0x52, 0x85, 0x7b, 0xa5, 0x60, 0xd9, 0xea, 0x3d, 0xcc, 0x6c, 0x9c, 0xc2, 0x05, 0x51, 0x9e, 0xa3,
+ 0x64, 0xe9, 0x0c, 0x17, 0x2c, 0x9d, 0x0f, 0x96, 0x61, 0xae, 0xb6, 0x77, 0x90, 0x0d, 0x5b, 0x4c,
+ 0xc7, 0x61, 0xa6, 0x62, 0x4d, 0xaa, 0x78, 0xb8, 0x4c, 0x45, 0x3e, 0x80, 0xd1, 0x72, 0x61, 0x39,
+ 0x5a, 0x7a, 0x6d, 0x60, 0x30, 0x97, 0x9b, 0xbb, 0x24, 0x0b, 0xd9, 0xfb, 0x56, 0x35, 0x77, 0xdf,
+ 0x32, 0xbf, 0x37, 0xe0, 0x6e, 0xb9, 0x0e, 0xc2, 0x04, 0xfd, 0x8a, 0x3a, 0x58, 0x10, 0x3e, 0xbb,
+ 0x99, 0x19, 0xa5, 0x37, 0xb3, 0xc8, 0x94, 0x54, 0xb9, 0xaa, 0xc1, 0x5f, 0x7b, 0x19, 0x97, 0x69,
+ 0xce, 0xd2, 0xb5, 0x7c, 0xbd, 0x7c, 0x67, 0x40, 0x7b, 0x85, 0xd8, 0xa9, 0x8a, 0x79, 0x01, 0x3b,
+ 0xc9, 0x1a, 0xb3, 0xb1, 0x3b, 0xf6, 0x19, 0x15, 0xa7, 0xaf, 0x6c, 0x1e, 0x52, 0xa1, 0xa6, 0xc4,
+ 0x66, 0xaf, 0xa5, 0x9b, 0x34, 0xd9, 0x6b, 0xfd, 0x58, 0xec, 0x59, 0x24, 0x65, 0x5d, 0xe7, 0xc5,
+ 0x0c, 0xf3, 0x75, 0x75, 0xc1, 0x7c, 0xc8, 0x4d, 0xa7, 0xa3, 0xfc, 0x74, 0xaa, 0xf7, 0xee, 0xaf,
+ 0x5c, 0x6a, 0xd9, 0x41, 0x76, 0x98, 0x1d, 0x64, 0xef, 0x2f, 0x85, 0x49, 0xa5, 0x27, 0x19, 0x74,
+ 0xe8, 0x18, 0x2e, 0xc4, 0x35, 0xa5, 0xeb, 0xb2, 0xbb, 0x7a, 0x5d, 0xc6, 0xfd, 0x94, 0x40, 0xa4,
+ 0xe7, 0x66, 0xef, 0xf5, 0x3a, 0xd4, 0xd5, 0x94, 0x92, 0x4f, 0x46, 0x44, 0xe1, 0x5a, 0xf1, 0x0b,
+ 0x09, 0xdd, 0x29, 0x52, 0x99, 0x7f, 0xf6, 0x35, 0xef, 0x2e, 0x91, 0x52, 0x6a, 0xdb, 0xc6, 0x43,
+ 0x03, 0xf9, 0xb0, 0x5d, 0xf6, 0xb2, 0x40, 0xf7, 0x8a, 0x60, 0xe6, 0x1f, 0x52, 0xcd, 0xbd, 0xa5,
+ 0x72, 0x29, 0x85, 0x79, 0xdf, 0x92, 0x3b, 0x65, 0xb1, 0x6f, 0xf9, 0x3b, 0x74, 0xb1, 0x6f, 0x73,
+ 0x17, 0x53, 0xa9, 0x8a, 0xc1, 0x4e, 0xe9, 0x16, 0x44, 0x85, 0x46, 0x17, 0x5c, 0x00, 0x9a, 0xed,
+ 0xe5, 0x82, 0x29, 0x9d, 0xdf, 0xe6, 0x1f, 0x9e, 0xf3, 0x25, 0x8e, 0x1e, 0x2c, 0xab, 0x9b, 0xac,
+ 0x01, 0x9d, 0x55, 0xc5, 0x53, 0x66, 0x1c, 0x02, 0x9a, 0x5f, 0xa1, 0x68, 0xe9, 0x76, 0x6d, 0x66,
+ 0xa6, 0xcf, 0xfe, 0xc7, 0xbf, 0xbe, 0x69, 0x19, 0x7f, 0xbc, 0x69, 0x19, 0x7f, 0xbd, 0x69, 0x19,
+ 0x2f, 0x7a, 0x63, 0x2a, 0x4e, 0xc3, 0x61, 0xc7, 0xf1, 0x5f, 0x75, 0xc7, 0x0c, 0x9f, 0x51, 0x55,
+ 0xc9, 0xd8, 0xed, 0x26, 0x3f, 0x55, 0xe0, 0x80, 0x66, 0x7e, 0xd1, 0x18, 0x6e, 0xc8, 0x3f, 0x1f,
+ 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x83, 0x41, 0x97, 0x10, 0x2f, 0x11, 0x00, 0x00,
}
func (m *RegisterUsingIAMMethodRequest) Marshal() (dAtA []byte, err error) {
@@ -2826,6 +2837,11 @@ func (m *RegisterUsingBoundKeypairRotationRequest) MarshalToSizedBuffer(dAtA []b
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
+ if m.SignatureAlgorithmSuite != 0 {
+ i = encodeVarintJoinservice(dAtA, i, uint64(m.SignatureAlgorithmSuite))
+ i--
+ dAtA[i] = 0x8
+ }
return len(dAtA) - i, nil
}
@@ -3473,6 +3489,9 @@ func (m *RegisterUsingBoundKeypairRotationRequest) Size() (n int) {
}
var l int
_ = l
+ if m.SignatureAlgorithmSuite != 0 {
+ n += 1 + sovJoinservice(uint64(m.SignatureAlgorithmSuite))
+ }
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -6189,6 +6208,25 @@ func (m *RegisterUsingBoundKeypairRotationRequest) Unmarshal(dAtA []byte) error
return fmt.Errorf("proto: RegisterUsingBoundKeypairRotationRequest: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
+ case 1:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field SignatureAlgorithmSuite", wireType)
+ }
+ m.SignatureAlgorithmSuite = 0
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowJoinservice
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ m.SignatureAlgorithmSuite |= types.SignatureAlgorithmSuite(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
default:
iNdEx = preIndex
skippy, err := skipJoinservice(dAtA[iNdEx:])
diff --git a/api/proto/teleport/legacy/client/proto/joinservice.proto b/api/proto/teleport/legacy/client/proto/joinservice.proto
index 145f67afdf0..2d7e2ac2f59 100644
--- a/api/proto/teleport/legacy/client/proto/joinservice.proto
+++ b/api/proto/teleport/legacy/client/proto/joinservice.proto
@@ -276,7 +276,8 @@ message RegisterUsingBoundKeypairCertificates {
// RegisterUsingBoundKeypairRotationRequest is the response sent by the server
// when a keypair rotation is required.
message RegisterUsingBoundKeypairRotationRequest {
- // This is a marker, no contents at this time.
+ // The signature algorithm suite in use by the cluster.
+ types.SignatureAlgorithmSuite signature_algorithm_suite = 1;
}
// RegisterUsingBoundKeypairMethodResponse is a response sent by the server
diff --git a/lib/auth/join/boundkeypair/boundkeypair.go b/lib/auth/join/boundkeypair/boundkeypair.go
index ffff96253ca..0e4407e4f8e 100644
--- a/lib/auth/join/boundkeypair/boundkeypair.go
+++ b/lib/auth/join/boundkeypair/boundkeypair.go
@@ -20,8 +20,13 @@ package boundkeypair
import (
"context"
+ "crypto"
+ "encoding/json"
+ "log/slog"
"os"
"path/filepath"
+ "sync"
+ "time"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
@@ -29,35 +34,60 @@ import (
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth/join"
"github.com/gravitational/teleport/lib/cryptosuites"
+ "github.com/gravitational/teleport/lib/sshutils"
)
const (
- PrivateKeyPath = "id_bkp"
- PublicKeyPath = PrivateKeyPath + ".pub"
- JoinStatePath = "bkp_state"
+ PrivateKeyPath = "id_bkp"
+ PublicKeyPath = PrivateKeyPath + ".pub"
+ JoinStatePath = "bkp_state"
+ KeyHistoryPath = "bkp_key_history.json"
+ KeyHistoryLength = 10
StandardFileWriteMode = 0600
)
+// KeyHistoryEntry records a private key and the timestamp it was generated.
+type KeyHistoryEntry struct {
+ // Time is the time this key was inserted into the history.
+ Time time.Time `json:"time"`
+
+ // PrivateKey is the private key, encoded in PEM format.
+ PrivateKey string `json:"private_key"`
+}
+
+// KeyHistory is a collection of `KeyHistoryEntry`.
+type KeyHistory struct {
+ Entries []KeyHistoryEntry `json:"entries"`
+}
+
// ClientState contains state parameters stored on disk needed to complete the
// bound keypair join process.
type ClientState struct {
+ mu sync.Mutex
+ fs FS
+
// PrivateKey is the parsed private key.
PrivateKey *keys.PrivateKey
- // PrivateKeyBytes contains the private key bytes. This value should always
- // be nonempty.
+ // PrivateKeyBytes contains the active private key bytes. This value should
+ // always be nonempty.
PrivateKeyBytes []byte
- // PublicKeyBytes contains the public key bytes. This value is not used at
- // runtime, and is only set when a public key should be written to disk,
- // like on first creation or during rotation. To consistently access the
- // public key, use `.PrivateKey.Public()`.
+ // PublicKeyBytes contains the active public key bytes. This value is not
+ // used at runtime, and is only set when a public key should be written to
+ // disk, like on first creation or during rotation. To consistently access
+ // the public key, use `.PrivateKey.Public()`.
PublicKeyBytes []byte
// JoinStateBytes contains join state bytes. This value will be empty if
// this client has not yet joined.
JoinStateBytes []byte
+
+ // KeyHistory records previous keypairs. In the event of a cluster rollback,
+ // this history will allow clients to rejoin if the cluster requests a
+ // keypair not currently marked as active.
+ KeyHistory []KeyHistoryEntry
}
// ToJoinParams creates joining parameters for use with `join.Register()` from
@@ -70,11 +100,28 @@ func (c *ClientState) ToJoinParams(initialJoinSecret string) *join.BoundKeypairP
}
return &join.BoundKeypairParams{
- // Note: pass the internal signer because go-jose does type assertions
- // on the standard library types.
- CurrentKey: c.PrivateKey.Signer,
PreviousJoinState: c.JoinStateBytes,
InitialJoinSecret: initialJoinSecret,
+ GetSigner: func(pubKey string) (crypto.Signer, error) {
+ return c.SignerForPublicKey([]byte(pubKey))
+ },
+ RequestNewKeypair: func(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (crypto.Signer, error) {
+ signer, err := c.GenerateKeypair(ctx, getSuite)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Make sure to store the intermediate state. We don't want to risk
+ // losing a private key if an error occurs between here and the end
+ // of the join process, but also don't want to force
+ // `GenerateKeypair()` to trigger a `Store()` on every call, so it's
+ // reasonably done here.
+ if err := c.Store(ctx); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return signer, nil
+ },
}
}
@@ -84,15 +131,26 @@ func (c *ClientState) UpdateFromRegisterResult(result *join.RegisterResult) erro
return trace.BadParameter("register result is missing bound keypair parameters")
}
- c.JoinStateBytes = result.BoundKeypair.JoinState
+ signer, err := c.SignerForPublicKey([]byte(result.BoundKeypair.BoundPublicKey))
+ if err != nil {
+ return trace.Wrap(err, "fetching key requested by auth")
+ }
- // TODO: When implementing rotation, use the bound public key value to set
- // the current public key.
+ if err := c.SetActiveKey(signer); err != nil {
+ return trace.Wrap(err, "setting new active key")
+ }
+
+ // The helpers above may lock the mutex, so don't lock it until we're
+ // touching fields directly.
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.JoinStateBytes = result.BoundKeypair.JoinState
return nil
}
-// ToPublicKeyBytes returns the public key bytes in ssh authorized_keys format.
+// ToPublicKeyBytes returns the active public key in ssh authorized_keys format.
func (c *ClientState) ToPublicKeyBytes() ([]byte, error) {
sshPubKey, err := ssh.NewPublicKey(c.PrivateKey.Public())
if err != nil {
@@ -102,6 +160,148 @@ func (c *ClientState) ToPublicKeyBytes() ([]byte, error) {
return ssh.MarshalAuthorizedKey(sshPubKey), nil
}
+// pubKeyEqual compares the two public keys per their `Equal()` implementation.
+func pubKeyEqual(a, b crypto.PublicKey) (bool, error) {
+ aEq, ok := a.(interface {
+ Equal(x crypto.PublicKey) bool
+ })
+ if !ok {
+ return false, trace.BadParameter("unsupported key type %T", a)
+ }
+
+ return aEq.Equal(b), nil
+}
+
+// SignerForPublicKey attempts to resolve a signer for the given public key
+// encoded in authorized_keys format.
+func (c *ClientState) SignerForPublicKey(authorizedKeysBytes []byte) (crypto.Signer, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ desiredPubKey, err := sshutils.CryptoPublicKey(authorizedKeysBytes)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Check the active key first.
+ activePubKeyBytes, err := c.ToPublicKeyBytes()
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ equal, err := pubKeyEqual(desiredPubKey, activePubKeyBytes)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ } else if equal {
+ // Parse a fresh copy of the key since this will escape the mutex and we
+ // can't be sure our local copy is thread safe.
+ key, err := keys.ParsePrivateKey(c.PrivateKeyBytes)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return key.Signer, nil
+ }
+
+ // Otherwise, search through the key history. If a keypair rotation was
+ // requested an `GenerateKeypair` was called, the new keypair should have
+ // been inserted at the top of this list.
+ for _, entry := range c.KeyHistory {
+ pk, err := keys.ParsePrivateKey([]byte(entry.PrivateKey))
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ equal, err := pubKeyEqual(desiredPubKey, pk.Signer.Public())
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ if equal {
+ return pk.Signer, nil
+ }
+ }
+
+ return nil, trace.NotFound("no matching key found")
+}
+
+// GenerateKeypair generates a new keypair, adds it to the key history, and
+// returns the resulting signer signer.
+func (c *ClientState) GenerateKeypair(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (crypto.Signer, error) {
+ key, err := cryptosuites.GenerateKey(ctx, getSuite, cryptosuites.BoundKeypairJoining)
+ if err != nil {
+ return nil, trace.Wrap(err, "generating keypair")
+ }
+
+ privateKeyBytes, err := keys.MarshalPrivateKey(key)
+ if err != nil {
+ return nil, trace.Wrap(err, "marshallng private key")
+ }
+
+ // prepend the new key to the top of the list for faster lookup
+ c.KeyHistory = append([]KeyHistoryEntry{{
+ Time: time.Now(),
+ PrivateKey: string(privateKeyBytes),
+ }}, c.KeyHistory...)
+
+ // Trim if necessary.
+ if len(c.KeyHistory) > KeyHistoryLength {
+ c.KeyHistory = c.KeyHistory[:min(len(c.KeyHistory), KeyHistoryLength)]
+ }
+
+ sshPubKey, err := ssh.NewPublicKey(key.Public())
+ if err != nil {
+ return nil, trace.Wrap(err, "creating ssh public key")
+ }
+
+ slog.InfoContext(ctx, "Generated new keypair", "public_key", string(ssh.MarshalAuthorizedKey(sshPubKey)))
+
+ return key, nil
+}
+
+// SetActiveKey updates the active keypair to reflect the given signer. Has no
+// effect if the active keypair's public key is already equal to the given
+// signer's public key, per its `Equals` implementation. Note that
+// `StoreClientState` still must be called after this to commit the changes to
+// the storage backend.
+func (c *ClientState) SetActiveKey(signer crypto.Signer) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ equal, err := pubKeyEqual(signer.Public(), c.PrivateKey.Public())
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if equal {
+ // nothing to do; specified key is already the active key
+ return nil
+ }
+
+ key, err := keys.NewPrivateKey(signer)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ privateKeyBytes, err := keys.MarshalPrivateKey(key.Signer)
+ if err != nil {
+ return trace.Wrap(err, "marshallng private key")
+ }
+
+ sshPubKey, err := ssh.NewPublicKey(key.Public())
+ if err != nil {
+ return trace.Wrap(err, "creating ssh public key")
+ }
+
+ c.PrivateKey = key
+ c.PrivateKeyBytes = privateKeyBytes
+ c.PublicKeyBytes = ssh.MarshalAuthorizedKey(sshPubKey)
+
+ slog.InfoContext(context.Background(), "Set new active keypair", "public_key", string(c.PublicKeyBytes))
+
+ return nil
+}
+
type FS interface {
Read(ctx context.Context, name string) ([]byte, error)
Write(ctx context.Context, name string, data []byte) error
@@ -133,6 +333,16 @@ func NewStandardFS(parentDir string) FS {
}
}
+// parseKeyHistory parses marshaled key history from JSON bytes
+func parseKeyHistory(data []byte) (KeyHistory, error) {
+ var history KeyHistory
+ if err := json.Unmarshal(data, &history); err != nil {
+ return KeyHistory{}, trace.Wrap(err)
+ }
+
+ return history, nil
+}
+
// LoadClientState attempts to load bound keypair client state from the given
// filesystem implementation. Callers should expect to handle NotFound errors
// returned here if a private key is not found; this indicates no prior client
@@ -158,46 +368,83 @@ func LoadClientState(ctx context.Context, fs FS) (*ClientState, error) {
return nil, trace.Wrap(err, "parsing private key")
}
- return &ClientState{
- PrivateKey: pk,
+ var keyHistory KeyHistory
+ keyHistoryBytes, err := fs.Read(ctx, KeyHistoryPath)
+ if trace.IsNotFound(err) {
+ // No history, this is allowed.
+ } else if err != nil {
+ slog.WarnContext(ctx, "unable to read key history, may be unable to recover in the event of a cluster rollback", "error", err)
+ } else if len(keyHistoryBytes) > 0 {
+ keyHistory, err = parseKeyHistory(keyHistoryBytes)
+ if err != nil {
+ slog.WarnContext(ctx, "unable to parse key history, may be unable to recover in the event of a cluster rollback", "error", err)
+ }
+ }
+ // If the key history is empty, initialize it with just the current key.
+ if len(keyHistory.Entries) == 0 {
+ keyHistory.Entries = []KeyHistoryEntry{{
+ Time: time.Now(),
+ PrivateKey: string(privateKeyBytes),
+ }}
+ }
+
+ return &ClientState{
+ fs: fs,
+
+ PrivateKey: pk,
PrivateKeyBytes: privateKeyBytes,
JoinStateBytes: joinStateBytes,
+ KeyHistory: keyHistory.Entries,
}, nil
}
// StoreClientState writes bound keypair client state to the given filesystem
// wrapper. Public keys and join state will only be written if
-func StoreClientState(ctx context.Context, fs FS, state *ClientState) error {
- if err := fs.Write(ctx, PrivateKeyPath, state.PrivateKeyBytes); err != nil {
+func (c *ClientState) Store(ctx context.Context) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if err := c.fs.Write(ctx, PrivateKeyPath, c.PrivateKeyBytes); err != nil {
return trace.Wrap(err, "writing private key")
}
- // TODO: maybe consider just not writing the public key at all. End users
- // aren't really meant to look in the internal storage, and we can just
- // derive the public key whenever we want.
-
// Only write the public key if it was explicitly provided. This helps save
// an unnecessary file write.
- if len(state.PublicKeyBytes) > 0 {
- if err := fs.Write(ctx, PublicKeyPath, state.PublicKeyBytes); err != nil {
+ if len(c.PublicKeyBytes) > 0 {
+ if err := c.fs.Write(ctx, PublicKeyPath, c.PublicKeyBytes); err != nil {
return trace.Wrap(err, "writing public key")
}
}
- if len(state.JoinStateBytes) > 0 {
- if err := fs.Write(ctx, JoinStatePath, state.JoinStateBytes); err != nil {
+ if len(c.JoinStateBytes) > 0 {
+ if err := c.fs.Write(ctx, JoinStatePath, c.JoinStateBytes); err != nil {
return trace.Wrap(err, "writing previous join state")
}
}
+ if len(c.KeyHistory) > 0 {
+ bytes, err := json.Marshal(KeyHistory{
+ Entries: c.KeyHistory,
+ })
+ if err != nil {
+ return trace.Wrap(err, "marshaling key key history")
+ }
+
+ if err := c.fs.Write(ctx, KeyHistoryPath, bytes); err != nil {
+ return trace.Wrap(err, "writing key history")
+ }
+ }
+
+ slog.DebugContext(ctx, "stored new bound keypair client state")
+
return nil
}
// NewUnboundClientState creates a new client state that has not yet been bound,
// i.e. a new keypair that has not been registered with Auth, and no prior join
// state.
-func NewUnboundClientState(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (*ClientState, error) {
+func NewUnboundClientState(ctx context.Context, fs FS, getSuite cryptosuites.GetSuiteFunc) (*ClientState, error) {
key, err := cryptosuites.GenerateKey(ctx, getSuite, cryptosuites.BoundKeypairJoining)
if err != nil {
return nil, trace.Wrap(err, "generating keypair")
@@ -220,9 +467,19 @@ func NewUnboundClientState(ctx context.Context, getSuite cryptosuites.GetSuiteFu
return nil, trace.Wrap(err)
}
+ history := []KeyHistoryEntry{
+ {
+ Time: time.Now(),
+ PrivateKey: string(privateKeyBytes),
+ },
+ }
+
return &ClientState{
+ fs: fs,
+
PrivateKeyBytes: privateKeyBytes,
PublicKeyBytes: publicKeyBytes,
PrivateKey: pk,
+ KeyHistory: history,
}, nil
}
diff --git a/lib/auth/join/boundkeypair/boundkeypair_test.go b/lib/auth/join/boundkeypair/boundkeypair_test.go
new file mode 100644
index 00000000000..325393d2698
--- /dev/null
+++ b/lib/auth/join/boundkeypair/boundkeypair_test.go
@@ -0,0 +1,127 @@
+/*
+ * 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 boundkeypair
+
+import (
+ "context"
+ "testing"
+
+ "github.com/gravitational/trace"
+ "github.com/stretchr/testify/require"
+
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/cryptosuites"
+)
+
+// memoryFS is a trivial in memory fs backend for testing use.
+type memoryFS struct {
+ files map[string][]byte
+ writes uint
+}
+
+func (f *memoryFS) Read(ctx context.Context, name string) ([]byte, error) {
+ data, ok := f.files[name]
+ if !ok {
+ return nil, trace.NotFound("not found: %s", name)
+ }
+
+ return data, nil
+}
+
+func (f *memoryFS) Write(ctx context.Context, name string, data []byte) error {
+ f.writes++
+ f.files[name] = data
+ return nil
+}
+
+func TestClientState(t *testing.T) {
+ ctx := context.Background()
+ fs := &memoryFS{
+ files: map[string][]byte{},
+ }
+
+ getSuite := cryptosuites.StaticAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1)
+
+ state, err := NewUnboundClientState(ctx, fs, getSuite)
+ require.NoError(t, err)
+
+ // Nothing should be written until `Store` is called explicitly
+ require.Empty(t, fs.files)
+
+ require.NoError(t, state.Store(ctx))
+ require.Len(t, fs.files, 3)
+ require.Len(t, state.KeyHistory, 1)
+ require.EqualValues(t, 3, fs.writes)
+
+ require.Equal(t, fs.files[PrivateKeyPath], state.PrivateKeyBytes)
+
+ // Keep the original key for later.
+ firstKey := state.PublicKeyBytes
+
+ prevKey := firstKey
+ expectWrites := 3
+
+ // Simulate writes up to the key history recording length.
+ for i := 0; i < KeyHistoryLength-1; i++ {
+ // We should still be able to load the original signer (< KeyHistoryLength)
+ _, err := state.SignerForPublicKey(firstKey)
+ require.NoError(t, err)
+
+ // Similarly, the previous key should still be accessible.
+ _, err = state.SignerForPublicKey(prevKey)
+ require.NoError(t, err)
+
+ prevKey = state.PrivateKey.MarshalSSHPublicKey()
+
+ // Generate a new keypair. It should be added to the history, but not marked
+ // as active.
+ signer, err := state.GenerateKeypair(ctx, getSuite)
+ require.NoError(t, err)
+ require.Len(t, state.KeyHistory, 2+i)
+ require.EqualValues(t, expectWrites, fs.writes, "no new writes expected")
+ require.Equal(t, fs.files[PrivateKeyPath], state.PrivateKeyBytes, "active key should not change on generation")
+ require.NotEqual(t, state.KeyHistory[0].PrivateKey, state.KeyHistory[1].PrivateKey)
+
+ // Now explicitly set the new active key
+ require.NoError(t, state.SetActiveKey(signer))
+ require.NotEqual(t, state.PublicKeyBytes, prevKey, "public key bytes must change for new key")
+
+ require.NoError(t, state.Store(ctx))
+ expectWrites += 3
+
+ // Load a fresh state for the next iteration.
+ state, err = LoadClientState(ctx, fs)
+ require.NoError(t, err)
+ }
+
+ // Generate a final keypair. This should push out the initial key. We'll
+ // have reached the history limit, so the total length should not change.
+ require.Len(t, state.KeyHistory, 10)
+ _, err = state.GenerateKeypair(ctx, getSuite)
+ require.NoError(t, err)
+ require.Len(t, state.KeyHistory, 10)
+
+ require.NoError(t, state.Store(ctx))
+ state, err = LoadClientState(ctx, fs)
+ require.NoError(t, err)
+
+ // Try to load the original key again; it should fail.
+ _, err = state.SignerForPublicKey(firstKey)
+ require.Error(t, err)
+}
diff --git a/lib/auth/join/join.go b/lib/auth/join/join.go
index d63c1ae3c9a..f5efad29af1 100644
--- a/lib/auth/join/join.go
+++ b/lib/auth/join/join.go
@@ -85,20 +85,34 @@ type GitlabParams struct {
EnvVarName string
}
+// GetSignerFunc is a function that fetches a keypair from bound keypair client
+// state.
+type GetSignerFunc func(pubKey string) (crypto.Signer, error)
+
+// KeygenFunc is a function to generate a new keypair for bound keypair joining.
+// Clients will generally need to store this for future use, so this function
+// should include some mechanism for storage and retrieval.
+type KeygenFunc func(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (crypto.Signer, error)
+
// BoundKeypairParams are parameters specific to bound-keypair joining.
type BoundKeypairParams struct {
// InitialJoinSecret is a one-time-use joining token for use on first join.
// May be unset if a keypair was registered with Auth out of band.
InitialJoinSecret string
- // CurrentKey is the keypair currently registered with Auth. On initial
- // join using `InitialJoinSecret`, this should be nil in favor of `NewKey`.
- CurrentKey crypto.Signer
-
// PreviousJoinState is the previous join state document provided by Auth
// alongside the previous set of certs. If this is initial registration, it
// can be empty.
PreviousJoinState []byte
+
+ // GetSigner is a function that fetches a signer from the client keystore.
+ GetSigner GetSignerFunc
+
+ // RequestNewKeypair is a callback function used to request a new keypair.
+ // This may be called at initial onboarding when `InitialJoinSecret` is set,
+ // or on any join (including the initial join) if `RotateAfter` is set on
+ // the backing token and its value has elapsed.
+ RequestNewKeypair KeygenFunc
}
// RegisterParams specifies parameters
@@ -947,47 +961,23 @@ func registerUsingBoundKeypairMethod(
) (*RegisterResult, error) {
bkParams := params.BoundKeypairParams
- // Build a map of all public keys to signers. At the moment, this is just
- // the current key, but may include e.g. previous and next keys for use in
- // case of a failed rotation attempt.
- // TODO: This implementation is likely to change when rotation is
- // implemented.
- signers := map[string]crypto.Signer{}
-
- if bkParams.CurrentKey != nil {
- pub, err := sshPubKeyFromSigner(bkParams.CurrentKey)
- if err != nil {
- return nil, trace.Wrap(err, "generating ssh public key from current key signer")
- }
-
- signers[pub] = bkParams.CurrentKey
- }
-
initReq := &proto.RegisterUsingBoundKeypairInitialRequest{
JoinRequest: registerUsingTokenRequestForParams(token, hostKeys, params),
InitialJoinSecret: bkParams.InitialJoinSecret,
PreviousJoinState: bkParams.PreviousJoinState,
}
- // TODO: When implementing rotation, we should make use of the returned
- // public key to ensure that key is marked as the primary.
regResponse, err := client.RegisterUsingBoundKeypairMethod(
ctx,
initReq,
func(resp *proto.RegisterUsingBoundKeypairMethodResponse) (*proto.RegisterUsingBoundKeypairMethodRequest, error) {
switch kind := resp.GetResponse().(type) {
case *proto.RegisterUsingBoundKeypairMethodResponse_Challenge:
- // Unlike other join methods, this function may be called multiple
- // times to complete challenges using one or both signers, so we'll
- // use the passed publicKey hint to resolve the proper signer to
- // use.
- signer, ok := signers[kind.Challenge.PublicKey]
- if !ok {
- return nil, trace.NotFound("could not complete challenge for unknown public key: %+#v", kind.Challenge.PublicKey)
+ signer, err := bkParams.GetSigner(kind.Challenge.PublicKey)
+ if err != nil {
+ return nil, trace.Wrap(err, "could not lookup signer for public key %+v", kind.Challenge.PublicKey)
}
- // TODO: might not be worth exporting this func; may be cheaper to
- // just copy the function here instead.
alg, err := jwt.AlgorithmForPublicKey(signer.Public())
if err != nil {
return nil, trace.Wrap(err, "determining signing algorithm for public key")
@@ -1022,8 +1012,29 @@ func registerUsingBoundKeypairMethod(
},
}, nil
case *proto.RegisterUsingBoundKeypairMethodResponse_Rotation:
- // TODO: Follow up implementation
- return nil, trace.NotImplemented("keypair rotation not yet implemented")
+ if bkParams.RequestNewKeypair == nil {
+ return nil, trace.BadParameter("RequestNewKeypair is required")
+ }
+
+ slog.InfoContext(ctx, "Server has requested keypair rotation", "suite", kind.Rotation.SignatureAlgorithmSuite)
+
+ newSigner, err := bkParams.RequestNewKeypair(ctx, cryptosuites.StaticAlgorithmSuite(kind.Rotation.SignatureAlgorithmSuite))
+ if err != nil {
+ return nil, trace.Wrap(err, "requesting new keypair")
+ }
+
+ newPubkey, err := sshPubKeyFromSigner(newSigner)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return &proto.RegisterUsingBoundKeypairMethodRequest{
+ Payload: &proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse{
+ RotationResponse: &proto.RegisterUsingBoundKeypairRotationResponse{
+ PublicKey: newPubkey,
+ },
+ },
+ }, nil
default:
// Note: certs variant is handled by RegisterUsingBoundKeypairMethod()
return nil, trace.BadParameter("received unexpected challenge response: %v", resp.GetResponse())
@@ -1033,6 +1044,7 @@ func registerUsingBoundKeypairMethod(
return nil, trace.Wrap(err)
}
+ // Implementation note, callers are expected to call
return &RegisterResult{
PrivateKey: hostKeys.privateKey,
Certs: regResponse.Certs,
diff --git a/lib/auth/join_bound_keypair.go b/lib/auth/join_bound_keypair.go
index f87d50bbc53..70dfcb78cb0 100644
--- a/lib/auth/join_bound_keypair.go
+++ b/lib/auth/join_bound_keypair.go
@@ -52,10 +52,6 @@ func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair)
return trace.BadParameter("bound keypair joining experiment is not enabled")
}
- if spec.RotateAfter != nil {
- return trace.NotImplemented("spec.bound_keypair.rotate_after is not yet implemented")
- }
-
if spec.Onboarding.RegistrationSecret != "" {
return trace.NotImplemented("spec.bound_keypair.onboarding.registration_secret is not yet implemented")
}
@@ -133,10 +129,10 @@ func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.Provis
// response.
func (a *Server) issueBoundKeypairChallenge(
ctx context.Context,
- marshalledKey string,
+ marshaledKey string,
challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
) error {
- key, err := libsshutils.CryptoPublicKey([]byte(marshalledKey))
+ key, err := libsshutils.CryptoPublicKey([]byte(marshaledKey))
if err != nil {
return trace.Wrap(err, "parsing bound public key")
}
@@ -175,7 +171,7 @@ func (a *Server) issueBoundKeypairChallenge(
response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{
Response: &proto.RegisterUsingBoundKeypairMethodResponse_Challenge{
Challenge: &proto.RegisterUsingBoundKeypairChallenge{
- PublicKey: marshalledKey,
+ PublicKey: marshaledKey,
Challenge: string(marshalledChallenge),
},
},
@@ -202,6 +198,98 @@ func (a *Server) issueBoundKeypairChallenge(
return nil
}
+// shouldRequestBoundKeypairRotation determines if a keypair rotation should be
+// requested given configured token field values.
+func shouldRequestBoundKeypairRotation(rotateAfter, lastRotatedAt *time.Time, now time.Time) bool {
+ if rotateAfter == nil {
+ // Field not set, nothing to do.
+ return false
+ }
+
+ if rotateAfter.After(now) {
+ // We haven't reached the rotation threshold, nothing to do.
+ return false
+ }
+
+ if lastRotatedAt == nil {
+ // There has not been a previous rotation, so rotate now.
+ return true
+ }
+
+ // Otherwise, rotate only if a rotation hasn't already taken place, i.e.
+ // `lastRotatedAt` is before the requested timestamp
+ return lastRotatedAt.Before(*rotateAfter)
+}
+
+// ensurePublicKeysNotEqual ensures the two public keys, in ssh authorized_keys
+// format, are parseable public keys and are not equal to one another. This can
+// be used to validate that clients actually provided a new key after receiving
+// a rotation request.
+func ensurePublicKeysNotEqual(a, b string) error {
+ aParsed, err := libsshutils.CryptoPublicKey([]byte(a))
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ aEq, ok := aParsed.(interface {
+ Equal(x crypto.PublicKey) bool
+ })
+ if !ok {
+ return trace.BadParameter("invalid public key type %T", aParsed)
+ }
+
+ bParsed, err := libsshutils.CryptoPublicKey([]byte(b))
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if aEq.Equal(bParsed) {
+ return trace.BadParameter("public key may not be reused after rotation")
+ }
+
+ return nil
+}
+
+// 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(
+ ctx context.Context,
+ challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc,
+) (string, error) {
+ cap, err := a.GetAuthPreference(ctx)
+ if err != nil {
+ return "", trace.Wrap(err)
+ }
+
+ a.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(),
+ },
+ },
+ })
+ 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 {
+ return "", trace.Wrap(err, "solving challenge for new public key")
+ }
+
+ return pubKey, nil
+}
+
// boundKeypairStatusMutator is a function called to mutate a bound keypair
// status during a call to PatchProvisionToken(). These functions may be called
// repeatedly if e.g. revision checks fail. To ensure invariants remain in
@@ -266,6 +354,38 @@ func mutateStatusBoundBotInstance(newBotInstance, expectPreviousBotInstance stri
}
}
+// mutateStatusLastRotatedAt updates the `status.LastRotatedAt` field to
+// indicate a keypair rotation has taken place. It ensures the previous value
+// has not changed before performing the update.
+func mutateStatusLastRotatedAt(newValue, expectPrevValue *time.Time) boundKeypairStatusMutator {
+ return func(_ *types.ProvisionTokenSpecV2BoundKeypair, status *types.ProvisionTokenStatusV2BoundKeypair) error {
+ switch {
+ case expectPrevValue == nil && status.LastRotatedAt == nil:
+ // no issue
+ case expectPrevValue != nil && status.LastRotatedAt == nil:
+ fallthrough
+ case expectPrevValue == nil && status.LastRotatedAt != nil:
+ fallthrough
+ case !expectPrevValue.Equal(*status.LastRotatedAt):
+ return trace.AccessDenied("unexpected backend state")
+ }
+
+ status.LastRotatedAt = newValue
+
+ return nil
+ }
+}
+
+// formatTimePointer stringifies a *time.Time for logging, but gracefully
+// handles nil values.
+func formatTimePointer(t *time.Time) string {
+ if t == nil {
+ return "nil"
+ }
+
+ return t.String()
+}
+
// RegisterUsingBoundKeypairMethod handles joining requests for the bound
// keypair join method. If successful, returns
func (a *Server) RegisterUsingBoundKeypairMethod(
@@ -308,6 +428,8 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
return nil, trace.BadParameter("specified join token is not for `%s` method", types.JoinMethodBoundKeypair)
}
+ log := a.logger.With("token", ptv2.GetName())
+
if ptv2.Status == nil {
ptv2.Status = &types.ProvisionTokenStatusV2{}
}
@@ -363,7 +485,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
return nil, trace.AccessDenied("previous join state is required but was not provided")
}
- a.logger.DebugContext(ctx, "join state verification required, verifying")
+ log.DebugContext(ctx, "join state verification required, verifying")
joinState, err = boundkeypair.VerifyJoinState(
ca,
string(req.PreviousJoinState),
@@ -377,7 +499,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// TODO: Once we have token-specific locking, generate a lock; this
// indicates the keypair may have been compromised.
// TODO: Audit log event for this.
- a.logger.ErrorContext(ctx, "bound keypair join state verification failed", "error", err)
+ log.ErrorContext(ctx, "bound keypair join state verification failed", "error", err)
return nil, trace.AccessDenied("join state verification failed")
}
@@ -386,7 +508,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
// instance is generated.
req.JoinRequest.PreviousBotInstanceID = joinState.BotInstanceID
- a.logger.DebugContext(ctx, "join state verified successfully", "join_state", joinState)
+ log.DebugContext(ctx, "join state verified successfully", "join_state", joinState)
// Note: we don't verify join state if it isn't expected. This is partly
// to ensure server-side recovery will work if join state desyncs - a
@@ -445,7 +567,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
if err := a.issueBoundKeypairChallenge(
ctx,
- spec.Onboarding.InitialPublicKey,
+ status.BoundPublicKey,
challengeResponse,
); err != nil {
return nil, trace.Wrap(err)
@@ -475,7 +597,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
expectNewBotInstance = true
default:
- a.logger.ErrorContext(
+ log.ErrorContext(
ctx, "unexpected state",
"has_bound_public_key", hasBoundPublicKey,
"has_bound_bot_instance", hasBoundBotInstance,
@@ -486,10 +608,32 @@ func (a *Server) RegisterUsingBoundKeypairMethod(
return nil, trace.BadParameter("unexpected state")
}
- if spec.RotateAfter != nil {
- // TODO, to be implemented in a future PR. `boundPublicKey` will need to
- // be updated.
- return nil, trace.NotImplemented("key rotation not yet supported")
+ // If we've crossed a keypair rotation threshold, request one now.
+ now := a.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)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Don't let clients provide the same key again.
+ if err := ensurePublicKeysNotEqual(boundPublicKey, newPubKey); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ mutators = append(mutators,
+ mutateStatusBoundPublicKey(newPubKey, boundPublicKey),
+ mutateStatusLastRotatedAt(&now, status.LastRotatedAt),
+ )
+
+ boundPublicKey = newPubKey
+
+ // TODO: Follow up with an audit log event for this.
}
certs, botInstanceID, err := a.generateCertsBot(
diff --git a/lib/auth/join_bound_keypair_test.go b/lib/auth/join_bound_keypair_test.go
index 0f641af6174..8c9795ab29c 100644
--- a/lib/auth/join_bound_keypair_test.go
+++ b/lib/auth/join_bound_keypair_test.go
@@ -26,6 +26,7 @@ import (
"github.com/go-jose/go-jose/v3/jwt"
"github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client"
@@ -96,9 +97,13 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
ctx := context.Background()
_, correctPublicKey := testBoundKeypair(t)
+ _, rotatedPublicKey := testBoundKeypair(t)
_, incorrectPublicKey := testBoundKeypair(t)
- srv := newTestTLSServer(t)
+ clock := clockwork.NewFakeClockAt(time.Now().Round(time.Second).UTC())
+ startTime := clock.Now()
+
+ srv := newTestTLSServer(t, withClock(clock))
auth := srv.Auth()
auth.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) {
return &mockBoundKeypairValidator{
@@ -225,20 +230,55 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
}
}
- makeSolver := func(publicKey string) client.RegisterUsingBoundKeypairChallengeResponseFunc {
- return func(challenge *proto.RegisterUsingBoundKeypairMethodResponse) (*proto.RegisterUsingBoundKeypairMethodRequest, error) {
+ type wrappedSolver struct {
+ rotatedPubKey string
+
+ rotationCount uint32
+ challengeCount uint32
+ solutions []string
+
+ wrapped client.RegisterUsingBoundKeypairChallengeResponseFunc
+ }
+
+ makeSolver := func(initialPubKey string, mutators ...func(s *wrappedSolver)) *wrappedSolver {
+ wrapper := &wrappedSolver{}
+ for _, mutator := range mutators {
+ mutator(wrapper)
+ }
+
+ wrapper.wrapped = func(challenge *proto.RegisterUsingBoundKeypairMethodResponse) (*proto.RegisterUsingBoundKeypairMethodRequest, error) {
switch r := challenge.Response.(type) {
case *proto.RegisterUsingBoundKeypairMethodResponse_Challenge:
- if r.Challenge.PublicKey != publicKey {
+ wrapper.challengeCount++
+
+ switch r.Challenge.PublicKey {
+ case initialPubKey:
+ case wrapper.rotatedPubKey:
+ default:
return nil, trace.BadParameter("wrong public key")
}
+ wrapper.solutions = append(wrapper.solutions, r.Challenge.PublicKey)
+
return &proto.RegisterUsingBoundKeypairMethodRequest{
Payload: &proto.RegisterUsingBoundKeypairMethodRequest_ChallengeResponse{
ChallengeResponse: &proto.RegisterUsingBoundKeypairChallengeResponse{
// For testing purposes, we'll just reply with the
// public key, to avoid needing to parse the JWT.
- Solution: []byte(publicKey),
+ Solution: []byte(r.Challenge.PublicKey),
+ },
+ },
+ }, nil
+ case *proto.RegisterUsingBoundKeypairMethodResponse_Rotation:
+ wrapper.rotationCount++
+ if wrapper.rotatedPubKey == "" {
+ return nil, trace.BadParameter("can't generate key")
+ }
+
+ return &proto.RegisterUsingBoundKeypairMethodRequest{
+ Payload: &proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse{
+ RotationResponse: &proto.RegisterUsingBoundKeypairRotationResponse{
+ PublicKey: wrapper.rotatedPubKey,
},
},
}, nil
@@ -246,17 +286,30 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
return nil, trace.BadParameter("invalid response type")
}
}
+
+ return wrapper
}
+ withRotatedPubKey := func(pubKey string) func(s *wrappedSolver) {
+ return func(s *wrappedSolver) {
+ s.rotatedPubKey = pubKey
+ }
+ }
+
+ // Advance the clock a bit. Tests may reference `startTime` for a past
+ // reference point.
+ clock.Advance(time.Hour)
+
tests := []struct {
name string
token types.ProvisionTokenV2
initReq *proto.RegisterUsingBoundKeypairInitialRequest
- solver client.RegisterUsingBoundKeypairChallengeResponseFunc
+ solver *wrappedSolver
- assertError require.ErrorAssertionFunc
- assertSuccess func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse)
+ assertError require.ErrorAssertionFunc
+ assertResponse func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse)
+ assertSolverState func(t *testing.T, s *wrappedSolver)
}{
{
// no bound key, no bound bot instance, aka initial join without
@@ -268,7 +321,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
solver: makeSolver(correctPublicKey),
assertError: require.NoError,
- assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
// join count should be incremented
require.Equal(t, uint32(1), v2.Status.BoundKeypair.RecoveryCount)
require.NotEmpty(t, v2.Status.BoundKeypair.BoundBotInstanceID)
@@ -303,7 +356,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
solver: makeSolver(correctPublicKey),
assertError: require.NoError,
- assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
// join count should not be incremented
require.Equal(t, uint32(0), v2.Status.BoundKeypair.RecoveryCount)
},
@@ -340,7 +393,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
solver: makeSolver(correctPublicKey),
assertError: require.NoError,
- assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, _ *client.BoundKeypairRegistrationResponse) {
require.Equal(t, uint32(1), v2.Status.BoundKeypair.RecoveryCount)
// Should generate a new bot instance
@@ -383,32 +436,13 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
require.ErrorContains(tt, err, "bot instance mismatch")
},
},
- {
- // TODO: rotation is not yet implemented.
- name: "rotation-requested",
-
- token: makeToken(func(v2 *types.ProvisionTokenV2) {
- t := time.Now()
- v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
- v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
- v2.Spec.BoundKeypair.RotateAfter = &t
- // TODO: test clock?
- }),
- initReq: makeInitReq(),
- solver: makeSolver(correctPublicKey),
-
- assertError: func(tt require.TestingT, err error, i ...interface{}) {
- require.Error(tt, err)
- require.ErrorContains(tt, err, "key rotation not yet supported")
- },
- },
{
name: "standard-initial-recovery-success",
token: makeToken(withRecovery("standard", 0, 1, "")),
initReq: makeInitReq(),
solver: makeSolver(correctPublicKey),
assertError: require.NoError,
- assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
require.Equal(t, uint32(1), v2.Status.BoundKeypair.RecoveryCount)
require.NotNil(t, res)
@@ -421,7 +455,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
initReq: makeInitReq(withJoinState(jwtSigner, withToken(withRecovery("standard", 1, 2, "id")))),
solver: makeSolver(correctPublicKey),
assertError: require.NoError,
- assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
require.Equal(t, uint32(2), v2.Status.BoundKeypair.RecoveryCount)
require.NotNil(t, res)
state := parseJoinState(t, res.JoinState)
@@ -502,7 +536,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
initReq: makeInitReq(withJoinState(jwtSigner, withToken(withRecovery("relaxed", 1, 0, "id")))),
solver: makeSolver(correctPublicKey),
assertError: require.NoError,
- assertSuccess: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
require.Equal(t, uint32(2), v2.Status.BoundKeypair.RecoveryCount)
require.NotNil(t, res)
@@ -512,28 +546,192 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) {
require.Equal(t, v2.Status.BoundKeypair.RecoveryCount, state.RecoverySequence)
},
},
+ {
+ // Initial rotation, i.e. `LastRotatedAt` isn't set. This should
+ // trigger as soon as the `RotateAfter` threshold has been crossed.
+ name: "first-rotation-success",
+
+ token: makeToken(func(v2 *types.ProvisionTokenV2) {
+ v2.Spec.BoundKeypair.RotateAfter = &startTime
+
+ v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
+ v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
+ }),
+ initReq: makeInitReq(),
+ solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
+
+ assertError: require.NoError,
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ require.Equal(t, rotatedPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
+ require.Equal(t, rotatedPublicKey, res.BoundPublicKey)
+ },
+ assertSolverState: func(t *testing.T, s *wrappedSolver) {
+ require.EqualValues(t, 2, s.challengeCount)
+ require.EqualValues(t, 1, s.rotationCount)
+ require.Equal(t, []string{correctPublicKey, rotatedPublicKey}, s.solutions)
+ },
+ },
+ {
+ // Initial rotation timestamp hasn't been reached
+ name: "first-rotation-skipped",
+
+ token: makeToken(func(v2 *types.ProvisionTokenV2) {
+ rotateAfter := clock.Now().Add(time.Minute)
+ v2.Spec.BoundKeypair.RotateAfter = &rotateAfter
+
+ v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
+ v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
+ }),
+ initReq: makeInitReq(),
+ solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
+
+ assertError: require.NoError,
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
+ require.Equal(t, correctPublicKey, res.BoundPublicKey)
+ },
+ assertSolverState: func(t *testing.T, s *wrappedSolver) {
+ require.EqualValues(t, 1, s.challengeCount)
+ require.EqualValues(t, 0, s.rotationCount)
+ require.Equal(t, []string{correctPublicKey}, s.solutions)
+ },
+ },
+ {
+ // This should only trigger after `RotateAfter` has been crossed and
+ // `LastRotatedAt` isn't after it.
+ name: "second-rotation-success",
+
+ token: makeToken(func(v2 *types.ProvisionTokenV2) {
+ rotateAfter := startTime.Add(10 * time.Minute)
+ v2.Spec.BoundKeypair.RotateAfter = &rotateAfter
+
+ v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
+ v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
+ v2.Status.BoundKeypair.LastRotatedAt = &startTime
+ }),
+ initReq: makeInitReq(),
+ solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
+
+ assertError: require.NoError,
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ require.Equal(t, rotatedPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
+ require.Equal(t, rotatedPublicKey, res.BoundPublicKey)
+ },
+ assertSolverState: func(t *testing.T, s *wrappedSolver) {
+ require.EqualValues(t, 2, s.challengeCount)
+ require.EqualValues(t, 1, s.rotationCount)
+ require.Equal(t, []string{correctPublicKey, rotatedPublicKey}, s.solutions)
+ },
+ },
+ {
+ // We shouldn't try to rotate again if LastRotatedAt is greater than
+ // RotateAfter.
+ name: "second-rotation-skipped",
+
+ token: makeToken(func(v2 *types.ProvisionTokenV2) {
+ v2.Spec.BoundKeypair.RotateAfter = &startTime
+
+ v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
+ v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
+
+ rotatedAt := startTime.Add(10 * time.Minute)
+ v2.Status.BoundKeypair.LastRotatedAt = &rotatedAt
+ }),
+ initReq: makeInitReq(),
+ solver: makeSolver(correctPublicKey, withRotatedPubKey(rotatedPublicKey)),
+
+ assertError: require.NoError,
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
+ require.Equal(t, correctPublicKey, res.BoundPublicKey)
+ },
+ assertSolverState: func(t *testing.T, s *wrappedSolver) {
+ require.EqualValues(t, 1, s.challengeCount)
+ require.EqualValues(t, 0, s.rotationCount)
+ require.Equal(t, []string{correctPublicKey}, s.solutions)
+ },
+ },
+ {
+ // If the client doesn't complete rotation, an error should be
+ // returned and the key should not change on the server.
+ name: "rotation-failure",
+
+ token: makeToken(func(v2 *types.ProvisionTokenV2) {
+ v2.Spec.BoundKeypair.RotateAfter = &startTime
+
+ v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
+ v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
+ }),
+ initReq: makeInitReq(),
+ solver: makeSolver(correctPublicKey),
+
+ assertError: func(tt require.TestingT, err error, i ...interface{}) {
+ require.ErrorContains(tt, err, "requesting a new public key")
+ },
+ assertSolverState: func(t *testing.T, s *wrappedSolver) {
+ require.EqualValues(t, 1, s.challengeCount)
+ require.EqualValues(t, 1, s.rotationCount)
+ require.Equal(t, []string{correctPublicKey}, s.solutions)
+ },
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
+ require.Nil(t, res)
+ },
+ },
+ {
+ name: "rotation-same-key-not-allowed",
+
+ token: makeToken(func(v2 *types.ProvisionTokenV2) {
+ v2.Spec.BoundKeypair.RotateAfter = &startTime
+
+ v2.Status.BoundKeypair.BoundPublicKey = correctPublicKey
+ v2.Status.BoundKeypair.BoundBotInstanceID = "asdf"
+ }),
+ initReq: makeInitReq(),
+ solver: makeSolver(correctPublicKey, withRotatedPubKey(correctPublicKey)),
+
+ assertError: func(tt require.TestingT, err error, i ...interface{}) {
+ require.ErrorContains(tt, err, "public key may not be reused after rotation")
+ },
+ assertSolverState: func(t *testing.T, s *wrappedSolver) {
+ require.EqualValues(t, 2, s.challengeCount)
+ require.EqualValues(t, 1, s.rotationCount)
+
+ // note: the client does complete the challenge for the
+ // duplicate key, but the attempt will ultimately be rejected
+ require.Equal(t, []string{correctPublicKey, correctPublicKey}, s.solutions)
+ },
+ assertResponse: func(t *testing.T, v2 *types.ProvisionTokenV2, res *client.BoundKeypairRegistrationResponse) {
+ require.Equal(t, correctPublicKey, v2.Status.BoundKeypair.BoundPublicKey)
+ require.Nil(t, res)
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token, err := types.NewProvisionTokenFromSpecAndStatus(
- tt.name, time.Now().Add(time.Minute), tt.token.Spec, tt.token.Status,
+ tt.name, time.Now().Add(2*time.Hour), tt.token.Spec, tt.token.Status,
)
require.NoError(t, err)
require.NoError(t, auth.CreateToken(ctx, token))
tt.initReq.JoinRequest.Token = tt.name
- response, err := auth.RegisterUsingBoundKeypairMethod(ctx, tt.initReq, tt.solver)
+ response, err := auth.RegisterUsingBoundKeypairMethod(ctx, tt.initReq, tt.solver.wrapped)
tt.assertError(t, err)
- if tt.assertSuccess != nil {
+ if tt.assertResponse != nil {
pt, err := auth.GetToken(ctx, tt.name)
require.NoError(t, err)
ptv2, ok := pt.(*types.ProvisionTokenV2)
require.True(t, ok)
- tt.assertSuccess(t, ptv2, response)
+ tt.assertResponse(t, ptv2, response)
+ }
+
+ if tt.assertSolverState != nil {
+ tt.assertSolverState(t, tt.solver)
}
})
}
diff --git a/lib/tbot/service_bot_identity.go b/lib/tbot/service_bot_identity.go
index 28409093a52..aabb97fe1db 100644
--- a/lib/tbot/service_bot_identity.go
+++ b/lib/tbot/service_bot_identity.go
@@ -477,7 +477,6 @@ func botIdentityFromToken(
}
// Only set during bound keypair joining, but used both before and after.
- var boundKeypairAdapter boundkeypair.FS
var boundKeypairState *boundkeypair.ClientState
switch params.JoinMethod {
@@ -494,8 +493,8 @@ func botIdentityFromToken(
case types.JoinMethodBoundKeypair:
joinSecret := cfg.Onboarding.BoundKeypair.InitialJoinSecret
- boundKeypairAdapter = config.NewBoundkeypairDestinationAdapter(cfg.Storage.Destination)
- boundKeypairState, err = boundkeypair.LoadClientState(ctx, boundKeypairAdapter)
+ adapter := config.NewBoundkeypairDestinationAdapter(cfg.Storage.Destination)
+ boundKeypairState, err = boundkeypair.LoadClientState(ctx, adapter)
if trace.IsNotFound(err) && joinSecret != "" {
return nil, trace.NotImplemented("no existing client state was found and join secrets are not yet supported")
} else if err != nil {
@@ -515,9 +514,7 @@ func botIdentityFromToken(
return nil, trace.Wrap(err)
}
- log.DebugContext(ctx, "updating bound keypair client state")
-
- if err := boundkeypair.StoreClientState(ctx, boundKeypairAdapter, boundKeypairState); err != nil {
+ if err := boundKeypairState.Store(ctx); err != nil {
return nil, trace.Wrap(err)
}
}
diff --git a/tool/tbot/keypair.go b/tool/tbot/keypair.go
index e91820a7be0..234f971fc38 100644
--- a/tool/tbot/keypair.go
+++ b/tool/tbot/keypair.go
@@ -121,13 +121,14 @@ func onKeypairCreateCommand(ctx context.Context, globals *cli.GlobalArgs, cmd *c
state, err = boundkeypair.NewUnboundClientState(
ctx,
+ fsAdapter,
getSuiteFromProxy(cmd.ProxyServer, globals.Insecure),
)
if err != nil {
return trace.Wrap(err, "initializing new client state")
}
- if err := boundkeypair.StoreClientState(ctx, fsAdapter, state); err != nil {
+ if err := state.Store(ctx); err != nil {
return trace.Wrap(err, "writing bound keypair state")
}
diff --git a/tool/tctl/common/bound_keypair_command.go b/tool/tctl/common/bound_keypair_command.go
new file mode 100644
index 00000000000..b9c80026b68
--- /dev/null
+++ b/tool/tctl/common/bound_keypair_command.go
@@ -0,0 +1,112 @@
+/*
+ * 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 common
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/alecthomas/kingpin/v2"
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport/api/mfa"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/auth/authclient"
+ "github.com/gravitational/teleport/lib/service/servicecfg"
+ commonclient "github.com/gravitational/teleport/tool/tctl/common/client"
+ tctlcfg "github.com/gravitational/teleport/tool/tctl/common/config"
+)
+
+type BoundKeypairCommand struct {
+ token string
+
+ requestRotation *kingpin.CmdClause
+}
+
+func (c *BoundKeypairCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) {
+ cmd := app.Command("bound-keypair", "Manage bound-keypair joining tokens")
+
+ c.requestRotation = cmd.Command("request-rotation", "Request a keypair rotation on the next join attempt.")
+ c.requestRotation.Arg("name", "The name of the token").Required().StringVar(&c.token)
+}
+
+func (c *BoundKeypairCommand) TryRun(ctx context.Context, cmd string, clientFunc commonclient.InitFunc) (match bool, err error) {
+ var commandFunc func(ctx context.Context, client *authclient.Client) error
+
+ switch cmd {
+ case c.requestRotation.FullCommand():
+ commandFunc = c.RequestRotation
+ default:
+ return false, nil
+ }
+
+ client, closeFn, err := clientFunc(ctx)
+ if err != nil {
+ return false, trace.Wrap(err)
+ }
+
+ err = commandFunc(ctx, client)
+ closeFn(ctx)
+
+ return true, trace.Wrap(err)
+}
+
+func (c *BoundKeypairCommand) RequestRotation(ctx context.Context, client *authclient.Client) error {
+ // Perform MFA checks now since we'll otherwise need to prompt twice.
+ if _, err := mfa.MFAResponseFromContext(ctx); err == nil {
+ // Nothing to do.
+ } else {
+ mfaResponse, err := mfa.PerformAdminActionMFACeremony(ctx, client.PerformMFACeremony, true /*allowReuse*/)
+ if err == nil {
+ ctx = mfa.ContextWithMFAResponse(ctx, mfaResponse)
+ } else if !errors.Is(err, &mfa.ErrMFANotRequired) && !errors.Is(err, &mfa.ErrMFANotSupported) {
+ return trace.Wrap(err)
+ }
+ }
+
+ token, err := client.GetToken(ctx, c.token)
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ if token.GetJoinMethod() != types.JoinMethodBoundKeypair {
+ return trace.BadParameter(
+ "token %s is of type %s, not %s",
+ c.token, token.GetJoinMethod(), types.JoinMethodBoundKeypair,
+ )
+ }
+
+ v2, ok := token.(*types.ProvisionTokenV2)
+ if !ok {
+ return trace.BadParameter("unsupported token type %T", token)
+ }
+
+ now := time.Now()
+ v2.Spec.BoundKeypair.RotateAfter = &now
+
+ if err := client.UpsertToken(ctx, v2); err != nil {
+ return trace.Wrap(err)
+ }
+
+ fmt.Printf("Token rotation flag has been set, rotation will be required during the next authentication attempt.\n")
+
+ return nil
+}
diff --git a/tool/tctl/common/cmds.go b/tool/tctl/common/cmds.go
index 9c28bcb3c83..21cf58600a9 100644
--- a/tool/tctl/common/cmds.go
+++ b/tool/tctl/common/cmds.go
@@ -72,5 +72,6 @@ func Commands() []CLICommand {
&AutoUpdateCommand{},
&stableunixusers.Command{},
&decision.Command{},
+ &BoundKeypairCommand{},
}
}