mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
274 lines
9.0 KiB
Go
274 lines
9.0 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gravitational/kingpin"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/native"
|
|
"github.com/gravitational/teleport/lib/client"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/service"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/sshutils"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/trace"
|
|
)
|
|
|
|
// AuthCommand implements `tctl auth` group of commands
|
|
type AuthCommand struct {
|
|
config *service.Config
|
|
authType string
|
|
genPubPath string
|
|
genPrivPath string
|
|
genUser string
|
|
genTTL time.Duration
|
|
exportAuthorityFingerprint string
|
|
exportPrivateKeys bool
|
|
output string
|
|
outputFormat client.IdentityFileFormat
|
|
compatVersion string
|
|
compatibility string
|
|
|
|
authGenerate *kingpin.CmdClause
|
|
authExport *kingpin.CmdClause
|
|
authSign *kingpin.CmdClause
|
|
}
|
|
|
|
// Initialize allows TokenCommand to plug itself into the CLI parser
|
|
func (a *AuthCommand) Initialize(app *kingpin.Application, config *service.Config) {
|
|
a.config = config
|
|
|
|
// operations with authorities
|
|
auth := app.Command("auth", "Operations with user and host certificate authorities (CAs)").Hidden()
|
|
a.authExport = auth.Command("export", "Export public cluster (CA) keys to stdout")
|
|
a.authExport.Flag("keys", "if set, will print private keys").BoolVar(&a.exportPrivateKeys)
|
|
a.authExport.Flag("fingerprint", "filter authority by fingerprint").StringVar(&a.exportAuthorityFingerprint)
|
|
a.authExport.Flag("compat", "export cerfiticates compatible with specific version of Teleport").StringVar(&a.compatVersion)
|
|
a.authExport.Flag("type", "certificate type: 'user' or 'host'").StringVar(&a.authType)
|
|
|
|
a.authGenerate = auth.Command("gen", "Generate a new SSH keypair").Hidden()
|
|
a.authGenerate.Flag("pub-key", "path to the public key").Required().StringVar(&a.genPubPath)
|
|
a.authGenerate.Flag("priv-key", "path to the private key").Required().StringVar(&a.genPrivPath)
|
|
|
|
a.authSign = auth.Command("sign", "Create an identity file(s) for a given user")
|
|
a.authSign.Flag("user", "Teleport user name").Required().StringVar(&a.genUser)
|
|
a.authSign.Flag("out", "identity output").Short('o').StringVar(&a.output)
|
|
a.authSign.Flag("format", "identity format: 'file' (default) or 'dir'").Default(string(client.DefaultIdentityFormat)).StringVar((*string)(&a.outputFormat))
|
|
a.authSign.Flag("ttl", "TTL (time to live) for the generated certificate").Default(fmt.Sprintf("%v", defaults.CertDuration)).DurationVar(&a.genTTL)
|
|
a.authSign.Flag("compat", "OpenSSH compatibility flag").StringVar(&a.compatibility)
|
|
}
|
|
|
|
// TryRun takes the CLI command as an argument (like "auth gen") and executes it
|
|
// or returns match=false if 'cmd' does not belong to it
|
|
func (a *AuthCommand) TryRun(cmd string, client *auth.TunClient) (match bool, err error) {
|
|
switch cmd {
|
|
case a.authGenerate.FullCommand():
|
|
err = a.GenerateKeys()
|
|
case a.authExport.FullCommand():
|
|
err = a.ExportAuthorities(client)
|
|
case a.authSign.FullCommand():
|
|
err = a.GenerateAndSignKeys(client)
|
|
|
|
default:
|
|
return false, nil
|
|
}
|
|
return true, trace.Wrap(err)
|
|
}
|
|
|
|
// ExportAuthorities outputs the list of authorities in OpenSSH compatible formats
|
|
// If --type flag is given, only prints keys for CAs of this type, otherwise
|
|
// prints all keys
|
|
func (a *AuthCommand) ExportAuthorities(client *auth.TunClient) error {
|
|
var typesToExport []services.CertAuthType
|
|
|
|
// if no --type flag is given, export all types
|
|
if a.authType == "" {
|
|
typesToExport = []services.CertAuthType{services.HostCA, services.UserCA}
|
|
} else {
|
|
authType := services.CertAuthType(a.authType)
|
|
if err := authType.Check(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
typesToExport = []services.CertAuthType{authType}
|
|
}
|
|
localAuthName, err := client.GetDomainName()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// fetch authorities via auth API (and only take local CAs, ignoring
|
|
// trusted ones)
|
|
var authorities []services.CertAuthority
|
|
for _, at := range typesToExport {
|
|
cas, err := client.GetCertAuthorities(at, a.exportPrivateKeys)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
for _, ca := range cas {
|
|
if ca.GetClusterName() == localAuthName {
|
|
authorities = append(authorities, ca)
|
|
}
|
|
}
|
|
}
|
|
|
|
// print:
|
|
for _, ca := range authorities {
|
|
if a.exportPrivateKeys {
|
|
for _, key := range ca.GetSigningKeys() {
|
|
fingerprint, err := sshutils.PrivateKeyFingerprint(key)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if a.exportAuthorityFingerprint != "" && fingerprint != a.exportAuthorityFingerprint {
|
|
continue
|
|
}
|
|
os.Stdout.Write(key)
|
|
fmt.Fprintf(os.Stdout, "\n")
|
|
}
|
|
} else {
|
|
for _, keyBytes := range ca.GetCheckingKeys() {
|
|
fingerprint, err := sshutils.AuthorizedKeyFingerprint(keyBytes)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if a.exportAuthorityFingerprint != "" && fingerprint != a.exportAuthorityFingerprint {
|
|
continue
|
|
}
|
|
|
|
// export certificates in the old 1.0 format where host and user
|
|
// certificate authorities were exported in the known_hosts format.
|
|
if a.compatVersion == "1.0" {
|
|
castr, err := hostCAFormat(ca, keyBytes, client)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
fmt.Println(castr)
|
|
continue
|
|
}
|
|
|
|
// export certificate authority in user or host ca format
|
|
var castr string
|
|
switch ca.GetType() {
|
|
case services.UserCA:
|
|
castr, err = userCAFormat(ca, keyBytes)
|
|
case services.HostCA:
|
|
castr, err = hostCAFormat(ca, keyBytes, client)
|
|
default:
|
|
return trace.BadParameter("unknown user type: %q", ca.GetType())
|
|
}
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// print the export friendly string
|
|
fmt.Println(castr)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateKeys generates a new keypair
|
|
func (a *AuthCommand) GenerateKeys() error {
|
|
keygen := native.New()
|
|
defer keygen.Close()
|
|
privBytes, pubBytes, err := keygen.GenerateKeyPair("")
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
err = ioutil.WriteFile(a.genPubPath, pubBytes, 0600)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
err = ioutil.WriteFile(a.genPrivPath, privBytes, 0600)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
fmt.Printf("wrote public key to: %v and private key to: %v\n", a.genPubPath, a.genPrivPath)
|
|
return nil
|
|
}
|
|
|
|
// GenerateAndSignKeys generates a new keypair and signs it for role
|
|
func (a *AuthCommand) GenerateAndSignKeys(clusterApi *auth.TunClient) error {
|
|
// parse compatibility parameter
|
|
compatibility, err := utils.CheckCompatibilityFlag(a.compatibility)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// generate a keypair:
|
|
key, err := client.NewKey()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// sign it and produce a cert:
|
|
key.Cert, err = clusterApi.GenerateUserCert(key.Pub, a.genUser, a.genTTL, compatibility)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// write the cert+private key to the output:
|
|
err = client.MakeIdentityFile(a.genUser, a.output, key, a.outputFormat)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if a.output != "" {
|
|
fmt.Printf("\nThe certificate has been written to %s\n", a.output)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// userCAFormat returns the certificate authority public key exported as a single
|
|
// line that can be placed in ~/.ssh/authorized_keys file. The format adheres to the
|
|
// man sshd (8) authorized_keys format, a space-separated list of: options, keytype,
|
|
// base64-encoded key, comment.
|
|
// For example:
|
|
//
|
|
// cert-authority AAA... type=user&clustername=cluster-a
|
|
//
|
|
// URL encoding is used to pass the CA type and cluster name into the comment field.
|
|
func userCAFormat(ca services.CertAuthority, keyBytes []byte) (string, error) {
|
|
comment := url.Values{
|
|
"type": []string{string(services.UserCA)},
|
|
"clustername": []string{ca.GetClusterName()},
|
|
}
|
|
|
|
return fmt.Sprintf("cert-authority %s %s", strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
|
|
}
|
|
|
|
// hostCAFormat returns the certificate authority public key exported as a single line
|
|
// that can be placed in ~/.ssh/authorized_hosts. The format adheres to the man sshd (8)
|
|
// authorized_hosts format, a space-separated list of: marker, hosts, key, and comment.
|
|
// For example:
|
|
//
|
|
// @cert-authority *.cluster-a ssh-rsa AAA... type=host
|
|
//
|
|
// URL encoding is used to pass the CA type and allowed logins into the comment field.
|
|
func hostCAFormat(ca services.CertAuthority, keyBytes []byte, client *auth.TunClient) (string, error) {
|
|
comment := url.Values{
|
|
"type": []string{string(ca.GetType())},
|
|
}
|
|
|
|
roles, err := services.FetchRoles(ca.GetRoles(), client, nil)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
allowedLogins, _ := roles.CheckLoginDuration(defaults.MinCertDuration + time.Second)
|
|
if len(allowedLogins) > 0 {
|
|
comment["logins"] = allowedLogins
|
|
}
|
|
|
|
return fmt.Sprintf("@cert-authority *.%s %s %s",
|
|
ca.GetClusterName(), strings.TrimSpace(string(keyBytes)), comment.Encode()), nil
|
|
}
|