Skip to content

Commit

Permalink
[tctl] Adds option to write tarred tctl auth sign output to stdout (#…
Browse files Browse the repository at this point in the history
…29451)

* Proof-of-concept writing tar to stdout

A quick and dirty experiment showing one possible approach for
writing certificates to stdout. Demonstrates a possible solution
to #29262.

DO NOT MERGE AS IS. IN NO WAY PRODUCTION READY.

* Fix timestamps

* Adds `--tar` option to `auth sign`

Adds an option to bundle the certificates generated by `tctl auth sign`
into a tarball and writes that tarball to stdout.

This is to facilitate extracting credentials from environments with
limited access to the filesystem and tools like a shell, tar and so
on, e.g. distroless Docker images.

Example usage:

```
$ kubectl exec ... -- tctl auth sign --user alice --format openssh -o alice --tar | tar xv
x alice-cert.pub
x alice
```

* Reroutes helper mesg to stderr when straming tar file to stdout
  • Loading branch information
tcsc authored Jul 26, 2023
1 parent c64cb74 commit 02d2f8b
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 19 deletions.
2 changes: 1 addition & 1 deletion lib/client/identityfile/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err
}
}

err = os.WriteFile(pubPath, caCerts, identityfile.FilePermissions)
err = writer.WriteFile(pubPath, caCerts, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down
103 changes: 85 additions & 18 deletions tool/tctl/common/auth_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
log "github.com/sirupsen/logrus"

"github.com/gravitational/teleport"
Expand Down Expand Up @@ -76,6 +77,8 @@ type AuthCommand struct {
signOverwrite bool
password string
caType string
streamTarfile bool
identityWriter identityfile.ConfigWriter

rotateGracePeriod time.Duration
rotateType string
Expand Down Expand Up @@ -114,8 +117,9 @@ func (a *AuthCommand) Initialize(app *kingpin.Application, config *servicecfg.Co
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)).
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.").
Expand All @@ -124,6 +128,7 @@ func (a *AuthCommand) Initialize(app *kingpin.Application, config *servicecfg.Co
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)
a.authSign.Flag("tar", "Create a tarball of the resulting certificates and stream to stdout.").BoolVar(&a.streamTarfile)
// --kube-cluster was an unfortunately chosen flag name, before teleport
// supported kubernetes_service and registered kubernetes clusters that are
// not trusted teleport clusters.
Expand Down Expand Up @@ -249,6 +254,12 @@ func (a *AuthCommand) GenerateKeys(ctx context.Context) error {

// GenerateAndSignKeys generates a new keypair and signs it for role
func (a *AuthCommand) GenerateAndSignKeys(ctx context.Context, clusterAPI auth.ClientI) error {
if a.streamTarfile {
tarWriter := newTarWriter(os.Stdout, clockwork.NewRealClock())
defer tarWriter.Close()
a.identityWriter = tarWriter
}

switch a.outputFormat {
case identityfile.FormatDatabase, identityfile.FormatMongo, identityfile.FormatCockroach,
identityfile.FormatRedis, identityfile.FormatElasticsearch:
Expand Down Expand Up @@ -325,6 +336,7 @@ func (a *AuthCommand) generateWindowsCert(ctx context.Context, clusterAPI auth.C
},
Format: a.outputFormat,
OverwriteDestination: a.signOverwrite,
Writer: a.identityWriter,
})
if err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -361,14 +373,14 @@ func (a *AuthCommand) generateSnowflakeKey(ctx context.Context, clusterAPI auth.
Key: key,
Format: a.outputFormat,
OverwriteDestination: a.signOverwrite,
Writer: a.identityWriter,
})
if err != nil {
return trace.Wrap(err)
}

return trace.Wrap(
writeHelperMessageDBmTLS(os.Stdout, filesWritten, "", a.outputFormat, ""),
)
writeHelperMessageDBmTLS(a.helperMsgDst(), filesWritten, "", a.outputFormat, "", a.streamTarfile))
}

// RotateCertAuthority starts or restarts certificate authority rotation process
Expand Down Expand Up @@ -477,11 +489,14 @@ func (a *AuthCommand) generateHostKeys(ctx context.Context, clusterAPI auth.Clie
Key: key,
Format: a.outputFormat,
OverwriteDestination: a.signOverwrite,
Writer: a.identityWriter,
})
if err != nil {
return trace.Wrap(err)
}
fmt.Printf("\nThe credentials have been written to %s\n", strings.Join(filesWritten, ", "))

fmt.Fprintf(a.helperMsgDst(), "\nThe credentials have been written to %s\n", strings.Join(filesWritten, ", "))

return nil
}

Expand Down Expand Up @@ -509,13 +524,14 @@ func (a *AuthCommand) generateDatabaseKeysForKey(ctx context.Context, clusterAPI
TTL: a.genTTL,
Key: key,
Password: a.password,
IdentityFileWriter: a.identityWriter,
}
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.password))
return trace.Wrap(writeHelperMessageDBmTLS(a.helperMsgDst(), filesWritten, a.output, a.outputFormat, a.password, a.streamTarfile))
}

var mapIdentityFileFormatHelperTemplate = map[identityfile.Format]*template.Template{
Expand All @@ -530,7 +546,7 @@ var mapIdentityFileFormatHelperTemplate = map[identityfile.Format]*template.Temp
identityfile.FormatOracle: oracleAuthSignTpl,
}

func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output string, outputFormat identityfile.Format, password string) error {
func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output string, outputFormat identityfile.Format, password string, tarOutput bool) error {
if writer == nil {
return nil
}
Expand All @@ -542,9 +558,10 @@ func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output st
return nil
}
tplVars := map[string]interface{}{
"files": strings.Join(filesWritten, ", "),
"password": password,
"output": output,
"files": strings.Join(filesWritten, ", "),
"password": password,
"output": output,
"tarOutput": tarOutput,
}
if outputFormat == defaults.ProtocolOracle {
tplVars["manualOrapkiFlow"] = len(filesWritten) != 1
Expand All @@ -556,7 +573,14 @@ func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output st

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}}.
dbAuthSignTpl = template.Must(template.New("").Parse(
`{{if .tarOutput }}
To unpack the tar archive, pipe the output of tctl to 'tar x'. For example:
$ tctl auth sign ${FLAGS} | tar -xv
{{else}}
Database credentials have been written to {{.files}}.
{{end}}
To enable mutual TLS on your PostgreSQL server, add the following to its postgresql.conf configuration file:
Expand All @@ -574,7 +598,14 @@ 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}}.
mongoAuthSignTpl = template.Must(template.New("").Parse(
`{{- if .tarOutput -}}
To unpack the tar archive, pipe the output of tctl to 'tar -x'. For example:
$ tctl auth sign ${FLAGS} | tar -x
{{- else -}}
Database credentials have been written to {{.files}}.
{{- end }}
To enable mutual TLS on your MongoDB server, add the following to its
mongod.yaml configuration file:
Expand All @@ -595,8 +626,15 @@ cockroach start \
# other flags...
`))

redisAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
redisAuthSignTpl = template.Must(template.New("").Parse(
`{{- if .tarOutput }}
Unpack the tar archive by piping the output of tctl to 'tar x'. For example:
$ tctl auth sign ${CERT_FLAGS} | tar -xv
{{else}}
Database credentials have been written to {{.files}}.
{{end}}
To enable mutual TLS on your Redis server, add the following to your redis.conf:
tls-ca-cert-file /path/to/{{.output}}.cas
Expand All @@ -611,7 +649,14 @@ 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}}.
elasticsearchAuthSignTpl = template.Must(template.New("").Parse(
`{{- if .tarOutput -}}
To unpack the tar archive, pipe the output of tctl to 'tar -x'. For example:
$ tctl auth sign ${FLAGS} | tar -x
{{- else -}}
Database credentials have been written to {{.files}}.
{{- end }}
To enable mutual TLS on your Elasticsearch server, add the following to your elasticsearch.yml:
Expand All @@ -632,7 +677,15 @@ 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}}.
cassandraAuthSignTpl = template.Must(template.New("").Parse(
`{{- if .tarOutput -}}
To unpack the tar archive, pipe the output of tctl to 'tar -x'. For example:
$ tctl auth sign ${FLAGS} | tar -x
{{- else -}}
Database credentials have been written to {{.files}}.
{{- end }}
To enable mutual TLS on your Cassandra server, add the following to your
cassandra.yaml configuration file:
client_encryption_options:
Expand All @@ -649,8 +702,13 @@ client_encryption_options:
cipher_suites: [TLS_RSA_WITH_AES_256_CBC_SHA]
`))

oracleAuthSignTpl = template.Must(template.New("").Parse(`
{{if .manualOrapkiFlow}}
oracleAuthSignTpl = template.Must(template.New("").Parse(
`{{- if .tarOutput -}}
To unpack the tar archive, pipe the output of tctl to 'tar -x'. For example:
$ tctl auth sign ${FLAGS} | tar -x
{{- end }}
{{- if .manualOrapkiFlow}}
Orapki binary was not found. Please create oracle wallet file manually by running the following commands on the Oracle server:
orapki wallet create -wallet {{.walletDir}} -auto_login_only
Expand Down Expand Up @@ -844,6 +902,7 @@ func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI auth.Clie
KubeClusterName: a.kubeCluster,
KubeTLSServerName: kubeTLSServerName,
OverwriteDestination: a.signOverwrite,
Writer: a.identityWriter,
})
if err != nil {
return trace.Wrap(err)
Expand All @@ -854,7 +913,8 @@ func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI auth.Clie
os.Stderr,
"\nGenerating credentials to allow a machine access to Teleport? We recommend Teleport's Machine ID! Find out more at https://goteleport.com/r/machineid-tip",
)
fmt.Printf("The credentials have been written to %s\n", strings.Join(filesWritten, ", "))

fmt.Fprintf(a.helperMsgDst(), "The credentials have been written to %s\n", strings.Join(filesWritten, ", "))

return nil
}
Expand Down Expand Up @@ -1072,3 +1132,10 @@ func getCertAuthTypes() []string {
}
return t
}

func (a *AuthCommand) helperMsgDst() io.Writer {
if a.streamTarfile {
return os.Stderr
}
return os.Stdout
}
81 changes: 81 additions & 0 deletions tool/tctl/common/tarwriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2023 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 (
"archive/tar"
"io"
"io/fs"
"os"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"

"github.com/gravitational/teleport/lib/client/identityfile"
)

// tarWriter implements a ConfigWriter that generates a tarfile from the
// files written to the config writer. Does not implement
type tarWriter struct {
tarball *tar.Writer
clock clockwork.Clock
}

// newTarWriter creates a new tarWriter that writes the generated tar
// file to the supplied `io.Writer`. Be sure to terminate the tar archive
// by calling `Close()` on the resuting `tarWriter`.
func newTarWriter(out io.Writer, clock clockwork.Clock) *tarWriter {
return &tarWriter{
tarball: tar.NewWriter(out),
clock: clock,
}
}

// Remove is not implemented, and only exists to fill out the
// `ConfigWriter` interface.
func (t *tarWriter) Remove(_ string) error {
return trace.NotImplemented("tarWriter.Remove()")
}

// Stat always returns `ErrNotExist` in ordre to sidestep the
// overwite check when writing certificates via a ConfigWriter.
func (t *tarWriter) Stat(_ string) (fs.FileInfo, error) {
return nil, os.ErrNotExist
}

// WriteFile adds the supplied content to the tar archive.
func (t *tarWriter) WriteFile(name string, content []byte, mode fs.FileMode) error {
header := &tar.Header{
Name: name,
Mode: int64(mode),
ModTime: t.clock.Now(),
Size: int64(len(content)),
}
if err := t.tarball.WriteHeader(header); err != nil {
return trace.Wrap(err)
}
if _, err := t.tarball.Write(content); err != nil {
return trace.Wrap(err)
}
return nil
}

// Close finalizes the tar archive, adding any necessary padding and footers.
func (t *tarWriter) Close() error {
return trace.Wrap(t.tarball.Close())
}

// identityfile.ConfigWriter implementation check
var _ identityfile.ConfigWriter = (*tarWriter)(nil)
Loading

0 comments on commit 02d2f8b

Please sign in to comment.