diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 8ca51721dcb6d..93b15b2face00 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -875,17 +875,12 @@ func (s *APIServer) generateUserCertBundle(auth ClientI, w http.ResponseWriter, }, nil } -type generateTokenReq struct { - Roles teleport.Roles `json:"roles"` - TTL time.Duration `json:"ttl"` -} - func (s *APIServer) generateToken(auth ClientI, w http.ResponseWriter, r *http.Request, _ httprouter.Params, version string) (interface{}, error) { - var req *generateTokenReq + var req GenerateTokenRequest if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } - token, err := auth.GenerateToken(req.Roles, req.TTL) + token, err := auth.GenerateToken(req) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 082456ca612d5..99ddc87f06614 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -557,20 +557,42 @@ func (s *AuthServer) CreateWebSession(user string) (services.WebSession, error) return sess, nil } -func (s *AuthServer) GenerateToken(roles teleport.Roles, ttl time.Duration) (string, error) { - for _, role := range roles { +// GenerateTokenRequest is a request to generate auth token +type GenerateTokenRequest struct { + // Token if provided sets the token value, otherwise will be auto generated + Token string `json:"token"` + // Roles is a list of roles this token authenticates as + Roles teleport.Roles `json:"roles"` + // TTL is a time to live for token + TTL time.Duration `json:"ttl"` +} + +// CheckAndSetDefaults checks and sets default values of request +func (req *GenerateTokenRequest) CheckAndSetDefaults() error { + for _, role := range req.Roles { if err := role.Check(); err != nil { - return "", trace.Wrap(err) + return trace.Wrap(err) } } - token, err := utils.CryptoRandomHex(TokenLenBytes) - if err != nil { + if req.Token == "" { + token, err := utils.CryptoRandomHex(TokenLenBytes) + if err != nil { + return trace.Wrap(err) + } + req.Token = token + } + return nil +} + +// GenerateToken generates multi-purpose authentication token +func (s *AuthServer) GenerateToken(req GenerateTokenRequest) (string, error) { + if err := req.CheckAndSetDefaults(); err != nil { return "", trace.Wrap(err) } - if err := s.Provisioner.UpsertToken(token, roles, ttl); err != nil { - return "", err + if err := s.Provisioner.UpsertToken(req.Token, req.Roles, req.TTL); err != nil { + return "", trace.Wrap(err) } - return token, nil + return req.Token, nil } // ClientCertPool returns trusted x509 cerificate authority pool diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 449fb2af42e97..30969fd615954 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -169,8 +169,8 @@ func (s *AuthSuite) TestTokensCRUD(c *C) { c.Assert(err, IsNil) c.Assert(len(btokens), Equals, 0) - // generate single-use token (TTL is 0) - tok, err := s.a.GenerateToken(teleport.Roles{teleport.RoleNode}, 0) + // generate persistent token + tok, err := s.a.GenerateToken(GenerateTokenRequest{Roles: teleport.Roles{teleport.RoleNode}}) c.Assert(err, IsNil) c.Assert(len(tok), Equals, 2*TokenLenBytes) @@ -198,8 +198,22 @@ func (s *AuthSuite) TestTokensCRUD(c *C) { roles, err = s.a.ValidateToken(tok) c.Assert(err, IsNil) + // generate predefined token + customToken := "custom token" + tok, err = s.a.GenerateToken(GenerateTokenRequest{Roles: teleport.Roles{teleport.RoleNode}, Token: customToken}) + c.Assert(err, IsNil) + c.Assert(tok, Equals, customToken) + + roles, err = s.a.ValidateToken(tok) + c.Assert(err, IsNil) + c.Assert(roles.Include(teleport.RoleNode), Equals, true) + c.Assert(roles.Include(teleport.RoleProxy), Equals, false) + + err = s.a.DeleteToken(customToken) + c.Assert(err, IsNil) + // generate multi-use token with long TTL: - multiUseToken, err := s.a.GenerateToken(teleport.Roles{teleport.RoleProxy}, time.Hour) + multiUseToken, err := s.a.GenerateToken(GenerateTokenRequest{Roles: teleport.Roles{teleport.RoleProxy}, TTL: time.Hour}) c.Assert(err, IsNil) _, err = s.a.ValidateToken(multiUseToken) c.Assert(err, IsNil) @@ -285,7 +299,7 @@ func (s *AuthSuite) TestBadTokens(c *C) { c.Assert(err, NotNil) // tampered - tok, err := s.a.GenerateToken(teleport.Roles{teleport.RoleAuth}, 0) + tok, err := s.a.GenerateToken(GenerateTokenRequest{Roles: teleport.Roles{teleport.RoleAuth}}) c.Assert(err, IsNil) tampered := string(tok[0]+1) + tok[1:] diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index acc0575a2ee07..fb24f24c9c420 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -214,11 +214,11 @@ func (a *AuthWithRoles) DeactivateCertAuthority(id services.CertAuthID) error { return trace.BadParameter("not implemented") } -func (a *AuthWithRoles) GenerateToken(roles teleport.Roles, ttl time.Duration) (string, error) { +func (a *AuthWithRoles) GenerateToken(req GenerateTokenRequest) (string, error) { if err := a.action(defaults.Namespace, services.KindToken, services.VerbCreate); err != nil { return "", trace.Wrap(err) } - return a.authServer.GenerateToken(roles, ttl) + return a.authServer.GenerateToken(req) } func (a *AuthWithRoles) RegisterUsingToken(req RegisterUsingTokenRequest) (*PackedKeys, error) { diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 893c2be814abf..90454b7fb940f 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -411,12 +411,10 @@ func (c *Client) DeactivateCertAuthority(id services.CertAuthID) error { // This token is used by SSH server to authenticate with Auth server // and get signed certificate and private key from the auth server. // -// The token can be used only once. -func (c *Client) GenerateToken(roles teleport.Roles, ttl time.Duration) (string, error) { - out, err := c.PostJSON(c.Endpoint("tokens"), generateTokenReq{ - Roles: roles, - TTL: ttl, - }) +// If token is not supplied, it will be auto generated and returned. +// If TTL is not supplied, token will be valid until removed. +func (c *Client) GenerateToken(req GenerateTokenRequest) (string, error) { + out, err := c.PostJSON(c.Endpoint("tokens"), req) if err != nil { return "", trace.Wrap(err) } @@ -2205,8 +2203,9 @@ type IdentityService interface { // This token is used by SSH server to authenticate with Auth server // and get signed certificate and private key from the auth server. // - // The token can be used only once. - GenerateToken(roles teleport.Roles, ttl time.Duration) (string, error) + // If token is not supplied, it will be auto generated and returned. + // If TTL is not supplied, token will be valid until removed. + GenerateToken(GenerateTokenRequest) (string, error) // GenerateKeyPair generates SSH private/public key pair optionally protected // by password. If the pass parameter is an empty string, the key pair diff --git a/lib/auth/tls_test.go b/lib/auth/tls_test.go index fe1fb478f48bb..33a58ef2f61c4 100644 --- a/lib/auth/tls_test.go +++ b/lib/auth/tls_test.go @@ -288,7 +288,7 @@ func (s *TLSSuite) TestTokens(c *check.C) { clt, err := s.server.NewClient(TestAdmin()) c.Assert(err, check.IsNil) - out, err := clt.GenerateToken(teleport.Roles{teleport.RoleNode}, 0) + out, err := clt.GenerateToken(GenerateTokenRequest{Roles: teleport.Roles{teleport.RoleNode}}) c.Assert(err, check.IsNil) c.Assert(len(out), check.Not(check.Equals), 0) } diff --git a/tool/tctl/common/node_command.go b/tool/tctl/common/node_command.go index 28f36f78a88db..c353dcfc0c3a9 100644 --- a/tool/tctl/common/node_command.go +++ b/tool/tctl/common/node_command.go @@ -34,9 +34,6 @@ import ( // NodeCommand implements `tctl nodes` group of commands type NodeCommand struct { config *service.Config - // count is optional hidden field that will cause - // tctl issue count tokens and output them in JSON format - count int // format is the output format, e.g. text or json format string // list of roles for the new node to assume @@ -46,6 +43,9 @@ type NodeCommand struct { ttl time.Duration // namespace is node namespace namespace string + // token is an optional custom token supplied by client, + // if not specified, is autogenerated + token string // CLI subcommands (clauses) nodeAdd *kingpin.CmdClause @@ -61,8 +61,8 @@ func (c *NodeCommand) Initialize(app *kingpin.Application, config *service.Confi c.nodeAdd = nodes.Command("add", "Generate a node invitation token") c.nodeAdd.Flag("roles", "Comma-separated list of roles for the new node to assume [node]").Default("node").StringVar(&c.roles) c.nodeAdd.Flag("ttl", "Time to live for a generated token").Default(defaults.ProvisioningTokenTTL.String()).DurationVar(&c.ttl) - c.nodeAdd.Flag("count", "add count tokens and output JSON with the list").Hidden().Default("1").IntVar(&c.count) - c.nodeAdd.Flag("format", "output format, 'text' or 'json'").Hidden().Default("text").StringVar(&c.format) + c.nodeAdd.Flag("token", "Custom token to use, autogenerated if not provided").StringVar(&c.token) + c.nodeAdd.Flag("format", "Output format, 'text' or 'json'").Hidden().Default("text").StringVar(&c.format) c.nodeAdd.Alias(AddNodeHelp) c.nodeList = nodes.Command("ls", "List all active SSH nodes within the cluster") @@ -84,24 +84,54 @@ func (c *NodeCommand) TryRun(cmd string, client auth.ClientI) (match bool, err e return true, trace.Wrap(err) } +const trustedClusterTemplate = `kind: trusted_cluster +version: v2 +metadata: + name: %v +spec: + enabled: true + token: %v + web_proxy_addr: proxy.example.com:3080 + role_map: + - remote: admin + local: [admin]` + +const trustedClusterMessage = `Trusted cluster token: %v + +Use this cluster in trusted cluster resource, for example: + +%v + +Please note: + + - This token will expire in %d minutes. + - Replace address proxy.example.com:3080 with externally accessible teleport proxy address. + - Set proper local and remote role_map property. +` + +const nodeMessage = `The invite token: %v + +Run this on the new node to join the cluster: + +> teleport start --roles=%s --token=%v --auth-server=%v + +Please note: + + - This invitation token will expire in %d minutes + - %v must be reachable from the new node, see --advertise-ip server flag +` + // Invite generates a token which can be used to add another SSH node // to a cluster func (c *NodeCommand) Invite(client auth.ClientI) error { - if c.count < 1 { - return trace.BadParameter("count should be > 0, got %v", c.count) - } // parse --roles flag roles, err := teleport.ParseRoles(c.roles) if err != nil { return trace.Wrap(err) } - var tokens []string - for i := 0; i < c.count; i++ { - token, err := client.GenerateToken(roles, c.ttl) - if err != nil { - return trace.Wrap(err) - } - tokens = append(tokens, token) + token, err := client.GenerateToken(auth.GenerateTokenRequest{Roles: roles, TTL: c.ttl, Token: c.token}) + if err != nil { + return trace.Wrap(err) } authServers, err := client.GetAuthServers() @@ -109,22 +139,27 @@ func (c *NodeCommand) Invite(client auth.ClientI) error { return trace.Wrap(err) } if len(authServers) == 0 { - return trace.Errorf("This cluster does not have any auth servers running") + return trace.Errorf("This cluster does not have any auth servers running.") + } + + clusterName, err := client.GetClusterName() + if err != nil { + return trace.Wrap(err) } // output format swtich: if c.format == "text" { - for _, token := range tokens { - fmt.Printf( - "The invite token: %v\nRun this on the new node to join the cluster:\n> teleport start --roles=%s --token=%v --auth-server=%v\n\nPlease note:\n", - token, strings.ToLower(roles.String()), token, authServers[0].GetAddr()) + if roles.Include(teleport.RoleTrustedCluster) { + trustedCluster := fmt.Sprintf(trustedClusterTemplate, clusterName.GetClusterName(), token) + fmt.Printf(trustedClusterMessage, token, trustedCluster, int(c.ttl.Minutes())) + } else { + fmt.Printf(nodeMessage, + token, strings.ToLower(roles.String()), token, authServers[0].GetAddr(), int(c.ttl.Minutes()), authServers[0].GetAddr()) } - fmt.Printf(" - This invitation token will expire in %d minutes\n", int(c.ttl.Minutes())) - fmt.Printf(" - %v must be reachable from the new node, see --advertise-ip server flag\n", authServers[0].GetAddr()) } else { - out, err := json.Marshal(tokens) + out, err := json.Marshal(token) if err != nil { - return trace.Wrap(err, "failed to marshal tokens") + return trace.Wrap(err, "failed to marshal token") } fmt.Printf(string(out)) }