Skip to content

Commit

Permalink
IAM join method support for tbot (#10535)
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen authored Mar 1, 2022
1 parent 19627ee commit 6e16ad6
Show file tree
Hide file tree
Showing 9 changed files with 843 additions and 690 deletions.
1,220 changes: 637 additions & 583 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion api/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,11 @@ message CreateBotResponse {
// TokenTTL is the TTL for the token. If it differs from the requested TTL,
// it may have been limited by server policy.
int64 TokenTTL = 4 [ (gogoproto.jsontag) = "ttl", (gogoproto.casttype) = "Duration" ];
// JoinMethod is the join method the bot must use to join the cluster.
string JoinMethod = 5 [
(gogoproto.jsontag) = "join_method",
(gogoproto.casttype) = "github.com/gravitational/teleport/api/types.JoinMethod"
];
}

// DeleteBotRequest is a request to delete a bot user
Expand Down Expand Up @@ -1446,7 +1451,8 @@ message ListResourcesRequest {
// SortBy describes which resource field and which direction to sort by.
types.SortBy SortBy = 8
[ (gogoproto.nullable) = false, (gogoproto.jsontag) = "sort_by,omitempty" ];
// NeedTotalCount indicates whether or not the caller also wants the total number of resources after filtering.
// NeedTotalCount indicates whether or not the caller also wants the total number of resources
// after filtering.
bool NeedTotalCount = 9 [ (gogoproto.jsontag) = "need_total_count,omitempty" ];
}

Expand Down
107 changes: 73 additions & 34 deletions lib/auth/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,6 @@ func createBotUser(ctx context.Context, s *Server, botName string, resourceName

// createBot creates a new certificate renewal bot from a bot request.
func (s *Server) createBot(ctx context.Context, req *proto.CreateBotRequest) (*proto.CreateBotResponse, error) {
if req.TokenID != "" {
// TODO: IAM joining for bots
return nil, trace.NotImplemented("IAM join for bots is not yet supported")
}

if req.Name == "" {
return nil, trace.BadParameter("bot name must not be empty")
}
Expand Down Expand Up @@ -148,30 +143,26 @@ func (s *Server) createBot(ctx context.Context, req *proto.CreateBotRequest) (*p
}
}

// Create the resources.
if _, err := createBotRole(ctx, s, req.Name, resourceName, req.Roles); err != nil {
provisionToken, err := s.checkOrCreateBotToken(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}

if _, err := createBotUser(ctx, s, req.Name, resourceName); err != nil {
// Create the resources.
if _, err := createBotRole(ctx, s, req.Name, resourceName, req.Roles); err != nil {
return nil, trace.Wrap(err)
}

ttl := time.Duration(req.TTL)
if ttl == 0 {
ttl = defaults.DefaultBotJoinTTL
}

provisionToken, err := s.CreateBotProvisionToken(ctx, resourceName, ttl)
if err != nil {
if _, err := createBotUser(ctx, s, req.Name, resourceName); err != nil {
return nil, trace.Wrap(err)
}

return &proto.CreateBotResponse{
TokenID: provisionToken.GetName(),
UserName: resourceName,
RoleName: resourceName,
TokenTTL: proto.Duration(ttl),
TokenID: provisionToken.GetName(),
UserName: resourceName,
RoleName: resourceName,
TokenTTL: proto.Duration(time.Until(*provisionToken.GetMetadata().Expires)),
JoinMethod: provisionToken.GetJoinMethod(),
}, nil
}

Expand Down Expand Up @@ -247,17 +238,55 @@ func (s *Server) getBotUsers(ctx context.Context) ([]types.User, error) {
return botUsers, nil
}

// CreateBotProvisionToken creates a new random dynamic provision token which
// allows bots to join with the given botName
func (s *Server) CreateBotProvisionToken(ctx context.Context, botName string, ttl time.Duration) (types.ProvisionToken, error) {
// checkOrCreateBotToken checks the existing token if given, or creates a new
// random dynamic provision token which allows bots to join with the given
// botName. Returns the token and any error.
func (s *Server) checkOrCreateBotToken(ctx context.Context, req *proto.CreateBotRequest) (types.ProvisionToken, error) {
resourceName := BotResourceName(req.Name)

// if the request includes a TokenID it should already exist
if req.TokenID != "" {
provisionToken, err := s.GetToken(ctx, req.TokenID)
if err != nil {
if trace.IsNotFound(err) {
return nil, trace.NotFound("token with name %q not found, create the token or do not set TokenName: %v",
req.TokenID, err)
}
return nil, trace.Wrap(err)
}
if !provisionToken.GetRoles().Include(types.RoleBot) {
return nil, trace.BadParameter("token %q is not valid for role %q",
req.TokenID, types.RoleBot)
}
if provisionToken.GetBotName() != resourceName {
return nil, trace.BadParameter("token %q is valid for bot with name %q, not %q",
req.TokenID, provisionToken.GetBotName(), resourceName)
}
switch provisionToken.GetJoinMethod() {
case types.JoinMethodToken, types.JoinMethodIAM:
default:
return nil, trace.BadParameter(
"token %q has join method %q which is not supported for bots. Supported join methods are %v",
req.TokenID, provisionToken.GetJoinMethod(), []types.JoinMethod{types.JoinMethodToken, types.JoinMethodIAM})
}
return provisionToken, nil
}

// create a new random dynamic token
tokenName, err := utils.CryptoRandomHex(TokenLenBytes)
if err != nil {
return nil, trace.Wrap(err)
}

ttl := time.Duration(req.TTL)
if ttl == 0 {
ttl = defaults.DefaultBotJoinTTL
}

tokenSpec := types.ProvisionTokenSpecV2{
Roles: []types.SystemRole{types.RoleBot},
JoinMethod: types.JoinMethodToken,
BotName: botName,
BotName: resourceName,
}
token, err := types.NewProvisionTokenFromSpec(tokenName, time.Now().Add(ttl), tokenSpec)
if err != nil {
Expand Down Expand Up @@ -315,6 +344,11 @@ func (s *Server) validateGenerationLabel(ctx context.Context, user types.User, c
)
}

// Sanity check that the requested generation is 1.
if certReq.generation != 1 {
return trace.BadParameter("explicitly requested generation %d is not equal to 1, this is a logic error", certReq.generation)
}

// Fetch a fresh copy of the user we can mutate safely. We can't
// implement a protobuf clone on User due to protobuf's proto.Clone()
// panicing when the user object has traits set, and a JSON
Expand Down Expand Up @@ -404,15 +438,14 @@ func (s *Server) validateGenerationLabel(ctx context.Context, user types.User, c
return nil
}

// generateInitialRenewableUserCerts is used to generate renewable bot certs
// and overlaps significantly with `generateUserCerts()`. However, it omits a
// number of options (impersonation, access requests, role requests, actual
// cert renewal, and most UserCertsRequest options that don't relate to bots)
// and does not care if the current identity is Nop.
// This function does not validate the current identity at all; the caller is
// expected to validate that the client is allowed to issue the renewable
// certificates.
func (s *Server) generateInitialRenewableUserCerts(ctx context.Context, username string, pubKey []byte, expires time.Time) (*proto.Certs, error) {
// generateInitialBotCerts is used to generate bot certs and overlaps
// significantly with `generateUserCerts()`. However, it omits a number of
// options (impersonation, access requests, role requests, actual cert renewal,
// and most UserCertsRequest options that don't relate to bots) and does not
// care if the current identity is Nop. This function does not validate the
// current identity at all; the caller is expected to validate that the client
// is allowed to issue the (possibly renewable) certificates.
func (s *Server) generateInitialBotCerts(ctx context.Context, username string, pubKey []byte, expires time.Time, renewable bool) (*proto.Certs, error) {
var err error

// Extract the user and role set for whom the certificate will be generated.
Expand Down Expand Up @@ -449,16 +482,22 @@ func (s *Server) generateInitialRenewableUserCerts(ctx context.Context, username
// add implicit roles to the set and build a checker
checker := services.NewRoleSet(parsedRoles...)

// renewable cert request must include a generation
var generation uint64
if renewable {
generation = 1
}

// Generate certificate
certReq := certRequest{
user: user,
ttl: expires.Sub(s.GetClock().Now()),
publicKey: pubKey,
checker: checker,
traits: user.GetTraits(),
renewable: true,
renewable: renewable,
includeHostCA: true,
generation: 1,
generation: generation,
}

if err := s.validateGenerationLabel(ctx, user, &certReq, 0); err != nil {
Expand Down
26 changes: 20 additions & 6 deletions lib/auth/join.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,34 @@ func (a *Server) generateCerts(ctx context.Context, provisionToken types.Provisi
botResourceName := provisionToken.GetBotName()
expires := a.GetClock().Now().Add(defaults.DefaultRenewableCertTTL)

certs, err := a.generateInitialRenewableUserCerts(ctx, botResourceName, req.PublicSSHKey, expires)
joinMethod := provisionToken.GetJoinMethod()

// certs for IAM method should not be renewable
var renewable bool
switch joinMethod {
case types.JoinMethodToken:
renewable = true
case types.JoinMethodIAM:
renewable = false
default:
return nil, trace.BadParameter("unsupported join method %q for bot", joinMethod)
}
certs, err := a.generateInitialBotCerts(ctx, botResourceName, req.PublicSSHKey, expires, renewable)
if err != nil {
return nil, trace.Wrap(err)
}

switch provisionToken.GetJoinMethod() {
case types.JoinMethodEC2, types.JoinMethodIAM:
// don't delete long-lived AWS join tokens
default:
// delete bot join tokens so they can't be re-used
switch joinMethod {
case types.JoinMethodToken:
// delete ephemeral bot join tokens so they can't be re-used
if err := a.DeleteToken(ctx, provisionToken.GetName()); err != nil {
log.WithError(err).Warnf("Could not delete bot provision token %q after generating certs",
string(backend.MaskKeyName(provisionToken.GetName())))
}
case types.JoinMethodIAM:
// don't delete long-lived IAM join tokens
default:
return nil, trace.BadParameter("unsupported join method %q for bot", joinMethod)
}

log.Infof("Bot %q has joined the cluster.", botResourceName)
Expand Down
18 changes: 13 additions & 5 deletions lib/auth/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ type RegisterParams struct {
// for TLS certificate verification.
// Defaults to real clock if unspecified
Clock clockwork.Clock
// EC2IdentityDocument is used for Simplified Node Joining to prove the
// identity of a joining EC2 instance.
EC2IdentityDocument []byte
// JoinMethod is the joining method used for this register request.
JoinMethod types.JoinMethod
// ec2IdentityDocument is used for Simplified Node Joining to prove the
// identity of a joining EC2 instance.
ec2IdentityDocument []byte
}

func (r *RegisterParams) setDefaults() {
Expand All @@ -142,6 +142,14 @@ func Register(params RegisterParams) (*proto.Certs, error) {
return nil, trace.Wrap(err)
}

// add EC2 Identity Document to params if required for given join method
if params.JoinMethod == types.JoinMethodEC2 {
params.ec2IdentityDocument, err = utils.GetEC2IdentityDocument()
if err != nil {
return nil, trace.Wrap(err)
}
}

log.WithField("auth-servers", params.Servers).Debugf("Registering node to the cluster.")

type registerMethod struct {
Expand Down Expand Up @@ -218,7 +226,7 @@ func registerThroughProxy(token string, params RegisterParams) (*proto.Certs, er
DNSNames: params.DNSNames,
PublicTLSKey: params.PublicTLSKey,
PublicSSHKey: params.PublicSSHKey,
EC2IdentityDocument: params.EC2IdentityDocument,
EC2IdentityDocument: params.ec2IdentityDocument,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -264,7 +272,7 @@ func registerThroughAuth(token string, params RegisterParams) (*proto.Certs, err
DNSNames: params.DNSNames,
PublicTLSKey: params.PublicTLSKey,
PublicSSHKey: params.PublicSSHKey,
EC2IdentityDocument: params.EC2IdentityDocument,
EC2IdentityDocument: params.ec2IdentityDocument,
})
}
return certs, trace.Wrap(err)
Expand Down
9 changes: 0 additions & 9 deletions lib/service/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,6 @@ func (process *TeleportProcess) firstTimeConnect(role types.SystemRole) (*Connec
return nil, trace.BadParameter("%v must join a cluster and needs a provisioning token", role)
}

var ec2IdentityDocument []byte
if process.Config.JoinMethod == types.JoinMethodEC2 {
ec2IdentityDocument, err = utils.GetEC2IdentityDocument()
if err != nil {
return nil, trace.Wrap(err)
}
}

process.log.Infof("Joining the cluster with a secure token.")
const reason = "first-time-connect"
keyPair, err := process.generateKeyPair(role, reason)
Expand All @@ -407,7 +399,6 @@ func (process *TeleportProcess) firstTimeConnect(role types.SystemRole) (*Connec
CAPath: filepath.Join(defaults.DataDir, defaults.CACertFile),
GetHostCredentials: client.HostCredentials,
Clock: process.Clock,
EC2IdentityDocument: ec2IdentityDocument,
JoinMethod: process.Config.JoinMethod,
})
if err != nil {
Expand Down
23 changes: 17 additions & 6 deletions tool/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import (
"strings"
"time"

"github.com/gravitational/teleport"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/trace"
)

const (
Expand Down Expand Up @@ -65,6 +67,10 @@ type CLIConf struct {
// CertificateTTL is the requested TTL of certificates. It should be some
// multiple of the renewal interval to allow for failed renewals.
CertificateTTL time.Duration

// JoinMethod is the method the bot should use to exchange a token for the
// initial certificate
JoinMethod string
}

// OnboardingConfig contains values only required on first connect.
Expand All @@ -78,6 +84,10 @@ type OnboardingConfig struct {
// CAPins is a list of certificate authority pins, used to validate the
// connection to the Teleport auth server.
CAPins []string `yaml:"ca_pins"`

// JoinMethod is the method the bot should use to exchange a token for the
// initial certificate
JoinMethod types.JoinMethod `yaml:"join_method"`
}

// BotConfig is the bot's root config object.
Expand Down Expand Up @@ -210,16 +220,17 @@ func FromCLIConf(cf *CLIConf) (*BotConfig, error) {
// (CAPath, CAPins, etc follow different codepaths so we don't want a
// situation where different fields become set weirdly due to struct
// merging)
if cf.Token != "" || len(cf.CAPins) > 0 {
if cf.Token != "" || len(cf.CAPins) > 0 || cf.JoinMethod != "" {
onboarding := config.Onboarding
if onboarding != nil && (onboarding.Token != "" || onboarding.CAPath != "" || len(onboarding.CAPins) > 0) {
if onboarding != nil && (onboarding.Token != "" || onboarding.CAPath != "" || len(onboarding.CAPins) > 0) || cf.JoinMethod != "" {
// To be safe, warn about possible confusion.
log.Warnf("CLI parameters are overriding onboarding config from %s", cf.ConfigPath)
}

config.Onboarding = &OnboardingConfig{
Token: cf.Token,
CAPins: cf.CAPins,
Token: cf.Token,
CAPins: cf.CAPins,
JoinMethod: types.JoinMethod(cf.JoinMethod),
}
}

Expand Down
Loading

0 comments on commit 6e16ad6

Please sign in to comment.