Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
716 changes: 647 additions & 69 deletions api/gen/proto/go/teleport/join/v1/joinservice.pb.go

Large diffs are not rendered by default.

102 changes: 101 additions & 1 deletion api/proto/teleport/join/v1/joinservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 9 additions & 2 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
148 changes: 148 additions & 0 deletions lib/auth/bound_keypair_tokens.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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))
}
12 changes: 3 additions & 9 deletions lib/auth/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package auth

import (
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
38 changes: 38 additions & 0 deletions lib/auth/join_bound_keypair_adapter.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
}
Loading
Loading