Files
teleport/tool/tctl/common/auth_command.go
T
8e05bcbb1b Export CRL and Database CA in DER format (#22578)
* feat(tctl): auth export type db der

* feat(tctl): add "crl" command to export revocation lists

* Apply suggestions from code review

Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com>

* chore(tctl): change command description

* Apply suggestions from code review

Co-authored-by: Marek Smoliński <marek@goteleport.com>

* feat(auth): enable role admin to call generate crl

---------

Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com>
Co-authored-by: Marek Smoliński <marek@goteleport.com>
2023-03-10 14:30:07 +00:00

1007 lines
34 KiB
Go

// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"context"
"fmt"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"text/template"
"time"
"github.com/gravitational/kingpin"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/keygen"
"github.com/gravitational/teleport/lib/auth/windows"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/db"
"github.com/gravitational/teleport/lib/client/identityfile"
"github.com/gravitational/teleport/lib/defaults"
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
// AuthCommand implements `tctl auth` group of commands
type AuthCommand struct {
config *servicecfg.Config
authType string
genPubPath string
genPrivPath string
genUser string
genHost string
format string
genTTL time.Duration
exportAuthorityFingerprint string
exportPrivateKeys bool
output string
outputFormat identityfile.Format
compatVersion string
compatibility string
proxyAddr string
leafCluster string
kubeCluster string
appName string
dbService string
dbName string
dbUser string
windowsUser string
windowsDomain string
windowsSID string
signOverwrite bool
jksPassword string
caType string
rotateGracePeriod time.Duration
rotateType string
rotateManualMode bool
rotateTargetPhase string
authGenerate *kingpin.CmdClause
authExport *kingpin.CmdClause
authSign *kingpin.CmdClause
authRotate *kingpin.CmdClause
authLS *kingpin.CmdClause
authCRL *kingpin.CmdClause
}
// Initialize allows TokenCommand to plug itself into the CLI parser
func (a *AuthCommand) Initialize(app *kingpin.Application, config *servicecfg.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 certificates compatible with specific version of Teleport").StringVar(&a.compatVersion)
a.authExport.Flag("type",
fmt.Sprintf("export certificate type (%v)", strings.Join(allowedCertificateTypes, ", "))).
EnumVar(&a.authType, allowedCertificateTypes...)
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").StringVar(&a.genUser)
a.authSign.Flag("host", "Teleport host name").StringVar(&a.genHost)
a.authSign.Flag("out", "Identity output").Short('o').Required().StringVar(&a.output)
a.authSign.Flag("format", fmt.Sprintf("Identity format: %s. %s is the default.",
identityfile.KnownFileFormats.String(), identityfile.DefaultFormat)).
Default(string(identityfile.DefaultFormat)).
StringVar((*string)(&a.outputFormat))
a.authSign.Flag("ttl", "TTL (time to live) for the generated certificate").
Default(fmt.Sprintf("%v", apidefaults.CertDuration)).
DurationVar(&a.genTTL)
a.authSign.Flag("compat", "OpenSSH compatibility flag").StringVar(&a.compatibility)
a.authSign.Flag("proxy", `Address of the teleport proxy. When --format is set to "kubernetes", this address will be set as cluster address in the generated kubeconfig file`).StringVar(&a.proxyAddr)
a.authSign.Flag("overwrite", "Whether to overwrite existing destination files. When not set, user will be prompted before overwriting any existing file.").BoolVar(&a.signOverwrite)
// --kube-cluster was an unfortunately chosen flag name, before teleport
// supported kubernetes_service and registered kubernetes clusters that are
// not trusted teleport clusters.
// It's kept as an alias for --leaf-cluster for backwards-compatibility,
// but hidden.
a.authSign.Flag("kube-cluster", `Leaf cluster to generate identity file for when --format is set to "kubernetes"`).Hidden().StringVar(&a.leafCluster)
a.authSign.Flag("leaf-cluster", `Leaf cluster to generate identity file for when --format is set to "kubernetes"`).StringVar(&a.leafCluster)
a.authSign.Flag("kube-cluster-name", `Kubernetes cluster to generate identity file for when --format is set to "kubernetes"`).StringVar(&a.kubeCluster)
a.authSign.Flag("app-name", `Application to generate identity file for. Mutually exclusive with "--db-service".`).StringVar(&a.appName)
a.authSign.Flag("db-service", `Database to generate identity file for. Mutually exclusive with "--app-name".`).StringVar(&a.dbService)
a.authSign.Flag("db-user", `Database user placed on the identity file. Only used when "--db-service" is set.`).StringVar(&a.dbUser)
a.authSign.Flag("db-name", `Database name placed on the identity file. Only used when "--db-service" is set.`).StringVar(&a.dbName)
a.authSign.Flag("windows-user", `Window user placed on the identity file. Only used when --format is set to "windows"`).StringVar(&a.windowsUser)
a.authSign.Flag("windows-domain", `Active Directory domain for which this cert is valid. Only used when --format is set to "windows"`).StringVar(&a.windowsDomain)
a.authSign.Flag("windows-sid", `Optional Security Identifier to embed in the certificate. Only used when --format is set to "windows"`).StringVar(&a.windowsSID)
a.authRotate = auth.Command("rotate", "Rotate certificate authorities in the cluster")
a.authRotate.Flag("grace-period", "Grace period keeps previous certificate authorities signatures valid, if set to 0 will force users to re-login and nodes to re-register.").
Default(fmt.Sprintf("%v", defaults.RotationGracePeriod)).
DurationVar(&a.rotateGracePeriod)
a.authRotate.Flag("manual", "Activate manual rotation , set rotation phases manually").BoolVar(&a.rotateManualMode)
a.authRotate.Flag("type", fmt.Sprintf("Certificate authority to rotate, one of: %s", strings.Join(getCertAuthTypes(), ", "))).EnumVar(&a.rotateType, getCertAuthTypes()...)
a.authRotate.Flag("phase", fmt.Sprintf("Target rotation phase to set, used in manual rotation, one of: %v", strings.Join(types.RotatePhases, ", "))).StringVar(&a.rotateTargetPhase)
a.authLS = auth.Command("ls", "List connected auth servers")
a.authLS.Flag("format", "Output format: 'yaml', 'json' or 'text'").Default(teleport.YAML).StringVar(&a.format)
a.authCRL = auth.Command("crl", "Export empty certificate revocation list (CRL) for certificate authorities")
a.authCRL.Flag("type", "certificate authority type").EnumVar(&a.caType, allowedCRLCertificateTypes...)
}
// 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(ctx context.Context, cmd string, client auth.ClientI) (match bool, err error) {
switch cmd {
case a.authGenerate.FullCommand():
err = a.GenerateKeys(ctx)
case a.authExport.FullCommand():
err = a.ExportAuthorities(ctx, client)
case a.authSign.FullCommand():
err = a.GenerateAndSignKeys(ctx, client)
case a.authRotate.FullCommand():
err = a.RotateCertAuthority(ctx, client)
case a.authLS.FullCommand():
err = a.ListAuthServers(ctx, client)
case a.authCRL.FullCommand():
err = a.GenerateCRLForCA(ctx, client)
default:
return false, nil
}
return true, trace.Wrap(err)
}
var allowedCertificateTypes = []string{
"user",
"host",
"tls-host",
"tls-user",
"tls-user-der",
"windows",
"db",
"db-der",
"openssh",
"saml-idp",
}
// allowedCRLCertificateTypes list of certificate authorities types that can
// have a CRL exported.
var allowedCRLCertificateTypes = []string{
string(types.HostCA),
string(types.DatabaseCA),
string(types.UserCA),
}
// 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(ctx context.Context, clt auth.ClientI) error {
exportFunc := client.ExportAuthorities
if a.exportPrivateKeys {
exportFunc = client.ExportAuthoritiesSecrets
}
authorities, err := exportFunc(
ctx,
clt,
client.ExportAuthoritiesRequest{
AuthType: a.authType,
ExportAuthorityFingerprint: a.exportAuthorityFingerprint,
UseCompatVersion: a.compatVersion == "1.0",
},
)
if err != nil {
return trace.Wrap(err)
}
fmt.Println(authorities)
return nil
}
// GenerateKeys generates a new keypair
func (a *AuthCommand) GenerateKeys(ctx context.Context) error {
keygen := keygen.New(ctx)
defer keygen.Close()
privBytes, pubBytes, err := keygen.GenerateKeyPair()
if err != nil {
return trace.Wrap(err)
}
err = os.WriteFile(a.genPubPath, pubBytes, 0o600)
if err != nil {
return trace.Wrap(err)
}
err = os.WriteFile(a.genPrivPath, privBytes, 0o600)
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(ctx context.Context, clusterAPI auth.ClientI) error {
switch a.outputFormat {
case identityfile.FormatDatabase, identityfile.FormatMongo, identityfile.FormatCockroach,
identityfile.FormatRedis, identityfile.FormatElasticsearch:
return a.generateDatabaseKeys(ctx, clusterAPI)
case identityfile.FormatCassandra, identityfile.FormatScylla:
jskPass, err := utils.CryptoRandomHex(32)
if err != nil {
return trace.Wrap(err)
}
a.jksPassword = jskPass
return a.generateDatabaseKeys(ctx, clusterAPI)
case identityfile.FormatSnowflake:
return a.generateSnowflakeKey(ctx, clusterAPI)
case identityfile.FormatWindows:
return a.generateWindowsCert(ctx, clusterAPI)
}
switch {
case a.genUser != "" && a.genHost == "":
return a.generateUserKeys(ctx, clusterAPI)
case a.genUser == "" && a.genHost != "":
return a.generateHostKeys(ctx, clusterAPI)
default:
return trace.BadParameter("--user or --host must be specified")
}
}
func (a *AuthCommand) generateWindowsCert(ctx context.Context, clusterAPI auth.ClientI) error {
var missingFlags []string
if len(a.windowsUser) == 0 {
missingFlags = append(missingFlags, "--windows-user")
}
if len(a.windowsDomain) == 0 {
missingFlags = append(missingFlags, "--windows-domain")
}
if len(missingFlags) > 0 {
return trace.BadParameter("the following flags are missing: %v",
strings.Join(missingFlags, ", "))
}
cn, err := clusterAPI.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
certDER, _, err := windows.GenerateWindowsDesktopCredentials(ctx, &windows.GenerateCredentialsRequest{
Username: a.windowsUser,
Domain: a.windowsDomain,
ActiveDirectorySID: a.windowsSID,
TTL: a.genTTL,
ClusterName: cn.GetClusterName(),
LDAPConfig: windows.LDAPConfig{Domain: a.windowsDomain},
AuthClient: clusterAPI,
})
if err != nil {
return trace.Wrap(err)
}
_, err = identityfile.Write(ctx, identityfile.WriteConfig{
OutputPath: a.output,
Key: &client.Key{
// the godocs say the map key is the desktop server name,
// but in this case we're just generating a cert that's not
// specific to a particular desktop
WindowsDesktopCerts: map[string][]byte{a.windowsUser: certDER},
},
Format: a.outputFormat,
OverwriteDestination: a.signOverwrite,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// generateSnowflakeKey exports DatabaseCA public key in the format required by Snowflake
// Ref: https://docs.snowflake.com/en/user-guide/key-pair-auth.html#step-2-generate-a-public-key
func (a *AuthCommand) generateSnowflakeKey(ctx context.Context, clusterAPI auth.ClientI) error {
key, err := client.GenerateRSAKey()
if err != nil {
return trace.Wrap(err)
}
cn, err := clusterAPI.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
certAuthID := types.CertAuthID{
Type: types.DatabaseCA,
DomainName: cn.GetClusterName(),
}
databaseCA, err := clusterAPI.GetCertAuthority(ctx, certAuthID, false)
if err != nil {
return trace.Wrap(err)
}
key.TrustedCerts = []auth.TrustedCerts{{TLSCertificates: services.GetTLSCerts(databaseCA)}}
filesWritten, err := identityfile.Write(ctx, identityfile.WriteConfig{
OutputPath: a.output,
Key: key,
Format: a.outputFormat,
OverwriteDestination: a.signOverwrite,
})
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(
writeHelperMessageDBmTLS(os.Stdout, filesWritten, "", a.outputFormat, ""),
)
}
// RotateCertAuthority starts or restarts certificate authority rotation process
func (a *AuthCommand) RotateCertAuthority(ctx context.Context, client auth.ClientI) error {
if a.rotateType == "" {
return trace.BadParameter("required flag --type not provided; previous versions defaulted to --type=all which is deprecated and will be removed in a future version")
}
if a.rotateType == string(types.CertAuthTypeAll) {
fmt.Println("\033[0;31mNOTICE:\033[0m --type=all will be deprecated in a future version")
}
req := auth.RotateRequest{
Type: types.CertAuthType(a.rotateType),
GracePeriod: &a.rotateGracePeriod,
TargetPhase: a.rotateTargetPhase,
}
if a.rotateManualMode {
req.Mode = types.RotationModeManual
} else {
req.Mode = types.RotationModeAuto
}
if err := client.RotateCertAuthority(ctx, req); err != nil {
return err
}
if a.rotateTargetPhase != "" {
fmt.Printf("Updated rotation phase to %q. To check status use 'tctl status'\n", a.rotateTargetPhase)
} else {
fmt.Printf("Initiated certificate authority rotation. To check status use 'tctl status'\n")
}
return nil
}
// ListAuthServers prints a list of connected auth servers
func (a *AuthCommand) ListAuthServers(ctx context.Context, clusterAPI auth.ClientI) error {
servers, err := clusterAPI.GetAuthServers()
if err != nil {
return trace.Wrap(err)
}
sc := &serverCollection{servers, false}
switch a.format {
case teleport.Text:
return sc.writeText(os.Stdout)
case teleport.YAML:
return writeYAML(sc, os.Stdout)
case teleport.JSON:
return writeJSON(sc, os.Stdout)
}
return nil
}
// GenerateCRLForCA generates a certificate revocation list for a certificate
// authority.
func (a *AuthCommand) GenerateCRLForCA(ctx context.Context, clusterAPI auth.ClientI) error {
certType := types.CertAuthType(a.caType)
if err := certType.Check(); err != nil {
return trace.Wrap(err)
}
crl, err := clusterAPI.GenerateCertAuthorityCRL(ctx, certType)
if err != nil {
return trace.Wrap(err)
}
fmt.Println(string(crl))
return nil
}
func (a *AuthCommand) generateHostKeys(ctx context.Context, clusterAPI auth.ClientI) error {
// only format=openssh is supported
if a.outputFormat != identityfile.FormatOpenSSH {
return trace.BadParameter("invalid --format flag %q, only %q is supported", a.outputFormat, identityfile.FormatOpenSSH)
}
// split up comma separated list
principals := strings.Split(a.genHost, ",")
// generate a keypair
key, err := client.GenerateRSAKey()
if err != nil {
return trace.Wrap(err)
}
cn, err := clusterAPI.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
clusterName := cn.GetClusterName()
key.Cert, err = clusterAPI.GenerateHostCert(ctx, key.MarshalSSHPublicKey(),
"", "", principals,
clusterName, types.RoleNode, 0)
if err != nil {
return trace.Wrap(err)
}
hostCAs, err := clusterAPI.GetCertAuthorities(ctx, types.HostCA, false)
if err != nil {
return trace.Wrap(err)
}
key.TrustedCerts = auth.AuthoritiesToTrustedCerts(hostCAs)
// if no name was given, take the first name on the list of principals
filePath := a.output
if filePath == "" {
filePath = principals[0]
}
filesWritten, err := identityfile.Write(ctx, identityfile.WriteConfig{
OutputPath: filePath,
Key: key,
Format: a.outputFormat,
OverwriteDestination: a.signOverwrite,
})
if err != nil {
return trace.Wrap(err)
}
fmt.Printf("\nThe credentials have been written to %s\n", strings.Join(filesWritten, ", "))
return nil
}
// generateDatabaseKeys generates a new unsigned key and signs it with Teleport
// CA for database access.
func (a *AuthCommand) generateDatabaseKeys(ctx context.Context, clusterAPI auth.ClientI) error {
key, err := client.GenerateRSAKey()
if err != nil {
return trace.Wrap(err)
}
return a.generateDatabaseKeysForKey(ctx, clusterAPI, key)
}
// generateDatabaseKeysForKey signs the provided unsigned key with Teleport CA
// for database access.
func (a *AuthCommand) generateDatabaseKeysForKey(ctx context.Context, clusterAPI auth.ClientI, key *client.Key) error {
principals := strings.Split(a.genHost, ",")
dbCertReq := db.GenerateDatabaseCertificatesRequest{
ClusterAPI: clusterAPI,
Principals: principals,
OutputFormat: a.outputFormat,
OutputCanOverwrite: a.signOverwrite,
OutputLocation: a.output,
TTL: a.genTTL,
Key: key,
JKSPassword: a.jksPassword,
}
filesWritten, err := db.GenerateDatabaseCertificates(ctx, dbCertReq)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(writeHelperMessageDBmTLS(os.Stdout, filesWritten, a.output, a.outputFormat, a.jksPassword))
}
var mapIdentityFileFormatHelperTemplate = map[identityfile.Format]*template.Template{
identityfile.FormatDatabase: dbAuthSignTpl,
identityfile.FormatMongo: mongoAuthSignTpl,
identityfile.FormatCockroach: cockroachAuthSignTpl,
identityfile.FormatRedis: redisAuthSignTpl,
identityfile.FormatSnowflake: snowflakeAuthSignTpl,
identityfile.FormatElasticsearch: elasticsearchAuthSignTpl,
identityfile.FormatCassandra: cassandraAuthSignTpl,
identityfile.FormatScylla: scyllaAuthSignTpl,
}
func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output string, outputFormat identityfile.Format, jksPassword string) error {
if writer == nil {
return nil
}
tpl, found := mapIdentityFileFormatHelperTemplate[outputFormat]
if !found {
// This format doesn't have a recommended configuration.
// Consider adding one to ease the installation for the end-user
return nil
}
tplVars := map[string]interface{}{
"files": strings.Join(filesWritten, ", "),
"jksPassword": jksPassword,
"output": output,
}
return trace.Wrap(tpl.Execute(writer, tplVars))
}
var (
// dbAuthSignTpl is printed when user generates credentials for a self-hosted database.
dbAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your PostgreSQL server, add the following to its postgresql.conf configuration file:
ssl = on
ssl_cert_file = '/path/to/{{.output}}.crt'
ssl_key_file = '/path/to/{{.output}}.key'
ssl_ca_file = '/path/to/{{.output}}.cas'
To enable mutual TLS on your MySQL server, add the following to its mysql.cnf configuration file:
[mysqld]
require_secure_transport=ON
ssl-cert=/path/to/{{.output}}.crt
ssl-key=/path/to/{{.output}}.key
ssl-ca=/path/to/{{.output}}.cas
`))
// mongoAuthSignTpl is printed when user generates credentials for a MongoDB database.
mongoAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your MongoDB server, add the following to its
mongod.yaml configuration file:
net:
tls:
mode: requireTLS
certificateKeyFile: /path/to/{{.output}}.crt
CAFile: /path/to/{{.output}}.cas
`))
cockroachAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your CockroachDB server, point it to the certs
directory using --certs-dir flag:
cockroach start \
--certs-dir={{.output}} \
# other flags...
`))
redisAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your Redis server, add the following to your redis.conf:
tls-ca-cert-file /path/to/{{.output}}.cas
tls-cert-file /path/to/{{.output}}.crt
tls-key-file /path/to/{{.output}}.key
tls-protocols "TLSv1.2 TLSv1.3"
`))
snowflakeAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
Please add the generated key to the Snowflake users as described here:
https://docs.snowflake.com/en/user-guide/key-pair-auth.html#step-4-assign-the-public-key-to-a-snowflake-user
`))
elasticsearchAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your Elasticsearch server, add the following to your elasticsearch.yml:
xpack.security.http.ssl:
certificate_authorities: /path/to/{{.output}}.cas
certificate: /path/to/{{.output}}.crt
key: /path/to/{{.output}}.key
enabled: true
client_authentication: required
verification_mode: certificate
xpack.security.authc.realms.pki.pki1:
order: 1
enabled: true
certificate_authorities: /path/to/{{.output}}.cas
For more information on configuring security settings in Elasticsearch, see:
https://www.elastic.co/guide/en/elasticsearch/reference/current/security-settings.html
`))
cassandraAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your Cassandra server, add the following to your
cassandra.yaml configuration file:
client_encryption_options:
enabled: true
optional: false
keystore: /path/to/{{.output}}.keystore
keystore_password: "{{.jksPassword}}"
require_client_auth: true
truststore: /path/to/{{.output}}.truststore
truststore_password: "{{.jksPassword}}"
protocol: TLS
algorithm: SunX509
store_type: JKS
cipher_suites: [TLS_RSA_WITH_AES_256_CBC_SHA]
`))
scyllaAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your Scylla server, add the following to your
scylla.yaml configuration file:
client_encryption_options:
enabled: true
certificate: /path/to/{{.output}}.crt
keyfile: /path/to/{{.output}}.key
truststore: /path/to/{{.output}}.cas
require_client_auth: True
`))
)
func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI auth.ClientI) error {
// Validate --proxy flag.
if err := a.checkProxyAddr(ctx, clusterAPI); err != nil {
return trace.Wrap(err)
}
// parse compatibility parameter
certificateFormat, err := utils.CheckCertificateFormatFlag(a.compatibility)
if err != nil {
return trace.Wrap(err)
}
// generate a keypair:
key, err := client.GenerateRSAKey()
if err != nil {
return trace.Wrap(err)
}
if a.leafCluster != "" {
if err := a.checkLeafCluster(clusterAPI); err != nil {
return trace.Wrap(err)
}
} else {
cn, err := clusterAPI.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
a.leafCluster = cn.GetClusterName()
}
key.ClusterName = a.leafCluster
if err := a.checkKubeCluster(ctx, clusterAPI); err != nil {
return trace.Wrap(err)
}
var (
routeToApp proto.RouteToApp
routeToDatabase proto.RouteToDatabase
certUsage proto.UserCertsRequest_CertUsage
)
// `appName` and `db` are mutually exclusive.
if a.appName != "" && a.dbService != "" {
return trace.BadParameter("only --app-name or --db-service can be set, not both")
}
switch {
case a.appName != "":
server, err := getApplicationServer(ctx, clusterAPI, a.appName)
if err != nil {
return trace.Wrap(err)
}
appSession, err := clusterAPI.CreateAppSession(ctx, types.CreateAppSessionRequest{
Username: a.genUser,
PublicAddr: server.GetApp().GetPublicAddr(),
ClusterName: a.leafCluster,
})
if err != nil {
return trace.Wrap(err)
}
routeToApp = proto.RouteToApp{
Name: a.appName,
PublicAddr: server.GetApp().GetPublicAddr(),
ClusterName: a.leafCluster,
SessionID: appSession.GetName(),
}
certUsage = proto.UserCertsRequest_App
case a.dbService != "":
server, err := getDatabaseServer(ctx, clusterAPI, a.dbService)
if err != nil {
return trace.Wrap(err)
}
routeToDatabase = proto.RouteToDatabase{
ServiceName: a.dbService,
Protocol: server.GetDatabase().GetProtocol(),
Database: a.dbName,
Username: a.dbUser,
}
certUsage = proto.UserCertsRequest_Database
}
reqExpiry := time.Now().UTC().Add(a.genTTL)
// Request signed certs from `auth` server.
certs, err := clusterAPI.GenerateUserCerts(ctx, proto.UserCertsRequest{
PublicKey: key.MarshalSSHPublicKey(),
Username: a.genUser,
Expires: reqExpiry,
Format: certificateFormat,
RouteToCluster: a.leafCluster,
KubernetesCluster: a.kubeCluster,
RouteToApp: routeToApp,
Usage: certUsage,
RouteToDatabase: routeToDatabase,
})
if err != nil {
return trace.Wrap(err)
}
key.Cert = certs.SSH
key.TLSCert = certs.TLS
hostCAs, err := clusterAPI.GetCertAuthorities(ctx, types.HostCA, false)
if err != nil {
return trace.Wrap(err)
}
key.TrustedCerts = auth.AuthoritiesToTrustedCerts(hostCAs)
// Is TLS routing enabled?
proxyListenerMode := types.ProxyListenerMode_Separate
if a.config != nil && a.config.Auth.NetworkingConfig != nil {
proxyListenerMode = a.config.Auth.NetworkingConfig.GetProxyListenerMode()
}
if networkConfig, err := clusterAPI.GetClusterNetworkingConfig(ctx); err == nil {
proxyListenerMode = networkConfig.GetProxyListenerMode()
}
// If we're in multiplexed mode get SNI name for kube from single multiplexed proxy addr
kubeTLSServerName := ""
if proxyListenerMode == types.ProxyListenerMode_Multiplex {
log.Debug("Using Proxy SNI for kube TLS server name")
u, err := parseURL(a.proxyAddr)
if err != nil {
return trace.Wrap(err)
}
// extract host part if present
split := strings.Split(u.Host, ":")
kubeTLSServerName = client.GetKubeTLSServerName(split[0])
}
expires, err := key.TeleportTLSCertValidBefore()
if err != nil {
log.WithError(err).Warn("Failed to check TTL validity")
// err swallowed on purpose
} else if reqExpiry.Sub(expires) > time.Minute {
maxAllowedTTL := time.Until(expires).Round(time.Second)
return trace.BadParameter(`The credential was not issued because the requested TTL of %s exceeded the maximum allowed value of %s. To successfully request a credential, please reduce the requested TTL.`,
a.genTTL,
maxAllowedTTL)
}
// write the cert+private key to the output:
filesWritten, err := identityfile.Write(ctx, identityfile.WriteConfig{
OutputPath: a.output,
Key: key,
Format: a.outputFormat,
KubeProxyAddr: a.proxyAddr,
KubeClusterName: a.kubeCluster,
KubeTLSServerName: kubeTLSServerName,
OverwriteDestination: a.signOverwrite,
})
if err != nil {
return trace.Wrap(err)
}
fmt.Printf("\nThe credentials have been written to %s\n", strings.Join(filesWritten, ", "))
return nil
}
func (a *AuthCommand) checkLeafCluster(clusterAPI auth.ClientI) error {
if a.outputFormat != identityfile.FormatKubernetes && a.leafCluster != "" {
// User set --cluster but it's not actually used for the chosen --format.
// Print a warning but continue.
fmt.Printf("Note: --cluster is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
}
if a.outputFormat != identityfile.FormatKubernetes {
return nil
}
clusters, err := clusterAPI.GetRemoteClusters()
if err != nil {
return trace.WrapWithMessage(err, "couldn't load leaf clusters")
}
for _, cluster := range clusters {
if cluster.GetMetadata().Name == a.leafCluster {
return nil
}
}
return trace.BadParameter("couldn't find leaf cluster named %q", a.leafCluster)
}
func (a *AuthCommand) checkKubeCluster(ctx context.Context, clusterAPI auth.ClientI) error {
if a.outputFormat != identityfile.FormatKubernetes && a.kubeCluster != "" {
// User set --kube-cluster-name but it's not actually used for the chosen --format.
// Print a warning but continue.
fmt.Printf("Note: --kube-cluster-name is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
}
if a.outputFormat != identityfile.FormatKubernetes {
return nil
}
localCluster, err := clusterAPI.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
if localCluster.GetClusterName() != a.leafCluster {
// Skip validation on remote clusters, since we don't know their
// registered kube clusters.
return nil
}
a.kubeCluster, err = kubeutils.CheckOrSetKubeCluster(ctx, clusterAPI, a.kubeCluster, a.leafCluster)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
return nil
}
func (a *AuthCommand) checkProxyAddr(ctx context.Context, clusterAPI auth.ClientI) error {
if a.outputFormat != identityfile.FormatKubernetes && a.proxyAddr != "" {
// User set --proxy but it's not actually used for the chosen --format.
// Print a warning but continue.
fmt.Printf("Note: --proxy is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
return nil
}
if a.outputFormat != identityfile.FormatKubernetes {
return nil
}
if a.proxyAddr != "" {
// User set --proxy. Validate it and set its scheme to https in case it was omitted.
u, err := parseURL(a.proxyAddr)
if err != nil {
return trace.WrapWithMessage(err, "specified --proxy URL is invalid")
}
switch u.Scheme {
case "":
u.Scheme = "https"
a.proxyAddr = u.String()
return nil
case "http", "https":
return nil
default:
return trace.BadParameter("expected --proxy URL with http or https scheme")
}
}
// User didn't specify --proxy for kubeconfig. Let's try to guess it.
//
// Is the auth server also a proxy?
if a.config.Proxy.Kube.Enabled {
var err error
if a.config.Auth.NetworkingConfig != nil &&
a.config.Auth.NetworkingConfig.GetProxyListenerMode() == types.ProxyListenerMode_Multiplex {
a.proxyAddr, err = a.config.Proxy.WebPublicAddr()
return trace.Wrap(err)
}
a.proxyAddr, err = a.config.Proxy.KubeAddr()
return trace.Wrap(err)
}
netConfig, err := clusterAPI.GetClusterNetworkingConfig(ctx)
if err != nil {
return trace.WrapWithMessage(err, "couldn't load cluster network configuration, try setting --proxy manually")
}
// Fetch proxies known to auth server and try to find a public address.
proxies, err := clusterAPI.GetProxies()
if err != nil {
return trace.WrapWithMessage(err, "couldn't load registered proxies, try setting --proxy manually")
}
for _, p := range proxies {
addr := p.GetPublicAddr()
if addr == "" {
continue
}
if netConfig.GetProxyListenerMode() == types.ProxyListenerMode_Multiplex {
u := url.URL{
Scheme: "https",
Host: addr,
}
a.proxyAddr = u.String()
return nil
}
uaddr, err := utils.ParseAddr(addr)
if err != nil {
log.Warningf("Invalid public address on the proxy %q: %q: %v.", p.GetName(), addr, err)
continue
}
u := url.URL{
Scheme: "https",
Host: net.JoinHostPort(uaddr.Host(), strconv.Itoa(defaults.KubeListenPort)),
}
a.proxyAddr = u.String()
return nil
}
return trace.BadParameter("couldn't find registered public proxies, specify --proxy when using --format=%q", identityfile.FormatKubernetes)
}
func parseURL(rawurl string) (*url.URL, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, trace.Wrap(err)
}
// If no scheme is provided url.Parse fails the parsing and considers the host
// as scheme, leaving the host empty.
if u.Host == "" {
return &url.URL{
Host: rawurl,
}, nil
}
return u, nil
}
func getApplicationServer(ctx context.Context, clusterAPI auth.ClientI, appName string) (types.AppServer, error) {
servers, err := clusterAPI.GetApplicationServers(ctx, apidefaults.Namespace)
if err != nil {
return nil, trace.Wrap(err)
}
for _, s := range servers {
if s.GetName() == appName {
return s, nil
}
}
return nil, trace.NotFound("app %q not found", appName)
}
// getDatabaseServer fetches a single `DatabaseServer` by name using the
// provided `auth.ClientI`.
func getDatabaseServer(ctx context.Context, clientAPI auth.ClientI, dbName string) (types.DatabaseServer, error) {
servers, err := clientAPI.GetDatabaseServers(ctx, apidefaults.Namespace)
if err != nil {
return nil, trace.Wrap(err)
}
for _, server := range servers {
if server.GetName() == dbName {
return server, nil
}
}
return nil, trace.NotFound("database %q not found", dbName)
}
func getCertAuthTypes() []string {
t := make([]string, 0, len(types.CertAuthTypes)+1)
for _, at := range types.CertAuthTypes {
t = append(t, string(at))
}
t = append(t, string(types.CertAuthTypeAll))
return t
}