Skip to content

Commit

Permalink
Add new config templates to tbot for databases and identity files (#…
Browse files Browse the repository at this point in the history
…11596) (#12500)

* Add new `identityfile` config template to `tbot`

This adds a new `identityfile` config template to tbot which
generates an identity file from any of the formats supported by
`tctl auth sign`.

It can be used by specifying one or more formats in the configuration
like so:

```yaml
destinations:
  - directory: /foo
    kinds: [ssh, tls]
    configs:
      - identityfile:
          formats: [file]
```

It requires both SSH and TLS certificates to work properly. App,
Kubernetes, and Database certs are unlikely to work as they have
additional cert requirements that will be added in separate PRs.

Multiple formats can be specified, and each will be written to its
own subdirectory within the destination using the name of the format.
The particular files written inside this directory depend on the
particular format selected, but n the above example, this means a
file named `/foo/file/identity` is written.

The files all have an `identity` prefix at the moment. This could be
made configurable if desired.

The `file` format can be used in conjunction with `tsh -i` and
`tctl -i` to use those tools with a tbot-generated identity.

Fixes #10812

* Make identityfile formats first-class config templates

This promotes most of the important identityfile formats to proper
config templates. User-facing `kinds` are removed to reduce confusion
and several config templates are now required.

 * The `ssh_client` template is now required and will be added
   automatically in all cases if not specified.
 * A new required `tls_cas` template is added to always export
   the current Teleport server CAs in a usable format.
 * A new required `identity` template is added to always export an
   identity file usable with tsh/tctl.
 * New optional `cockroach`, `mongo`, and `tls` templates can export
   specifically-formatted TLS certs for various databases and apps.

Additionally some other changes were caught during testing:
 * `botfs` now allows users to specify if files should be opened for
   reading or for writing; previously, written files were never
   truncated when opened for writing leading to garbage at the end of
   files if the length changed. Truncation isn't sane for reading so
   the two use-cases are now split.

* Update lib/client/identityfile/identity.go

Co-authored-by: Jakub Nyckowski <[email protected]>

* Address first batch of review comments

Tweaked the `botfs.openStandard` and `botfs.openSecure` functions to
accept a plain file mode, and removed a ton of boilerplate in
`configtemplate.go`.

* Fix problematic nil interface check in configtemplate

* Clarify comment about `client.Key` DB certs

* Address review feedback

 - Use `DatabaseCA` for database specific templates; make the `tls`
   template's CA configurable; write the database CA alongside the
   others.
 - Simplify nil interface check

* Fix outdated var names

Co-authored-by: Jakub Nyckowski <[email protected]>

Co-authored-by: Jakub Nyckowski <[email protected]>
  • Loading branch information
timothyb89 and jakule authored May 10, 2022
1 parent 382577a commit a880a39
Show file tree
Hide file tree
Showing 27 changed files with 1,101 additions and 219 deletions.
10 changes: 10 additions & 0 deletions api/identityfile/identityfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ func Write(idFile *IdentityFile, path string) error {
return nil
}

// Encode encodes the given identityFile to bytes.
func Encode(idFile *IdentityFile) ([]byte, error) {
buf := new(bytes.Buffer)
if err := encodeIdentityFile(buf, idFile); err != nil {
return nil, trace.Wrap(err)
}

return buf.Bytes(), nil
}

// Read reads an identity file from generic io.Reader interface.
func Read(r io.Reader) (*IdentityFile, error) {
ident, err := decodeIdentityFile(r)
Expand Down
81 changes: 64 additions & 17 deletions lib/client/identityfile/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package identityfile
import (
"context"
"fmt"
"io/ioutil"
"io/fs"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -91,6 +91,40 @@ func (f FormatList) String() string {
return strings.Join(elems, ", ")
}

// ConfigWriter is a simple filesystem abstraction to allow alternative simple
// read/write for this package.
type ConfigWriter interface {
// WriteFile writes the given data to path `name`, using the specified
// permissions if the file is new.
WriteFile(name string, data []byte, perm os.FileMode) error

// Remove removes a file.
Remove(name string) error

// Stat fetches information about a file.
Stat(name string) (fs.FileInfo, error)
}

// StandardConfigWriter is a trivial ConfigWriter that wraps the relevant `os` functions.
type StandardConfigWriter struct{}

// WriteFile writes data to the named file, creating it if necessary.
func (s *StandardConfigWriter) WriteFile(name string, data []byte, perm os.FileMode) error {
return os.WriteFile(name, data, perm)
}

// Remove removes the named file or (empty) directory.
// If there is an error, it will be of type *PathError.
func (s *StandardConfigWriter) Remove(name string) error {
return os.Remove(name)
}

// Stat returns a FileInfo describing the named file.
// If there is an error, it will be of type *PathError.
func (s *StandardConfigWriter) Stat(name string) (fs.FileInfo, error) {
return os.Stat(name)
}

// WriteConfig holds the necessary information to write an identity file.
type WriteConfig struct {
// OutputPath is the output path for the identity file. Note that some
Expand All @@ -108,11 +142,19 @@ type WriteConfig struct {
// overwritten. When false, user will be prompted for confirmation of
// overwrite first.
OverwriteDestination bool
// Writer is the filesystem implementation.
Writer ConfigWriter
}

// Write writes user credentials to disk in a specified format.
// It returns the names of the files successfully written.
func Write(cfg WriteConfig) (filesWritten []string, err error) {
// If no writer was set, use the standard implementation.
writer := cfg.Writer
if writer == nil {
writer = &StandardConfigWriter{}
}

if cfg.OutputPath == "" {
return nil, trace.BadParameter("identity output path is not specified")
}
Expand All @@ -121,7 +163,7 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
// dump user identity into a single file:
case FormatFile:
filesWritten = append(filesWritten, cfg.OutputPath)
if err := checkOverwrite(cfg.OverwriteDestination, filesWritten...); err != nil {
if err := checkOverwrite(writer, cfg.OverwriteDestination, filesWritten...); err != nil {
return nil, trace.Wrap(err)
}

Expand All @@ -146,7 +188,12 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
idFile.CACerts.TLS = append(idFile.CACerts.TLS, ca.TLSCertificates...)
}

if err := identityfile.Write(idFile, cfg.OutputPath); err != nil {
idBytes, err := identityfile.Encode(idFile)
if err != nil {
return nil, trace.Wrap(err)
}

if err := writer.WriteFile(cfg.OutputPath, idBytes, identityfile.FilePermissions); err != nil {
return nil, trace.Wrap(err)
}

Expand All @@ -155,16 +202,16 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
keyPath := cfg.OutputPath
certPath := keypaths.IdentitySSHCertPath(keyPath)
filesWritten = append(filesWritten, keyPath, certPath)
if err := checkOverwrite(cfg.OverwriteDestination, filesWritten...); err != nil {
if err := checkOverwrite(writer, cfg.OverwriteDestination, filesWritten...); err != nil {
return nil, trace.Wrap(err)
}

err = ioutil.WriteFile(certPath, cfg.Key.Cert, identityfile.FilePermissions)
err = writer.WriteFile(certPath, cfg.Key.Cert, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}

err = ioutil.WriteFile(keyPath, cfg.Key.Priv, identityfile.FilePermissions)
err = writer.WriteFile(keyPath, cfg.Key.Priv, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -182,16 +229,16 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
}

filesWritten = append(filesWritten, keyPath, certPath, casPath)
if err := checkOverwrite(cfg.OverwriteDestination, filesWritten...); err != nil {
if err := checkOverwrite(writer, cfg.OverwriteDestination, filesWritten...); err != nil {
return nil, trace.Wrap(err)
}

err = ioutil.WriteFile(certPath, cfg.Key.TLSCert, identityfile.FilePermissions)
err = writer.WriteFile(certPath, cfg.Key.TLSCert, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}

err = ioutil.WriteFile(keyPath, cfg.Key.Priv, identityfile.FilePermissions)
err = writer.WriteFile(keyPath, cfg.Key.Priv, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -201,7 +248,7 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
caCerts = append(caCerts, cert...)
}
}
err = ioutil.WriteFile(casPath, caCerts, identityfile.FilePermissions)
err = writer.WriteFile(casPath, caCerts, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -212,10 +259,10 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
certPath := cfg.OutputPath + ".crt"
casPath := cfg.OutputPath + ".cas"
filesWritten = append(filesWritten, certPath, casPath)
if err := checkOverwrite(cfg.OverwriteDestination, filesWritten...); err != nil {
if err := checkOverwrite(writer, cfg.OverwriteDestination, filesWritten...); err != nil {
return nil, trace.Wrap(err)
}
err = ioutil.WriteFile(certPath, append(cfg.Key.TLSCert, cfg.Key.Priv...), identityfile.FilePermissions)
err = writer.WriteFile(certPath, append(cfg.Key.TLSCert, cfg.Key.Priv...), identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -225,21 +272,21 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
caCerts = append(caCerts, cert...)
}
}
err = ioutil.WriteFile(casPath, caCerts, identityfile.FilePermissions)
err = writer.WriteFile(casPath, caCerts, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}

case FormatKubernetes:
filesWritten = append(filesWritten, cfg.OutputPath)
if err := checkOverwrite(cfg.OverwriteDestination, filesWritten...); err != nil {
if err := checkOverwrite(writer, cfg.OverwriteDestination, filesWritten...); err != nil {
return nil, trace.Wrap(err)
}
// Clean up the existing file, if it exists.
//
// kubeconfig.Update would try to parse it and merge in new
// credentials, which is not what we want.
if err := os.Remove(cfg.OutputPath); err != nil && !os.IsNotExist(err) {
if err := writer.Remove(cfg.OutputPath); err != nil && !os.IsNotExist(err) {
return nil, trace.Wrap(err)
}

Expand All @@ -257,11 +304,11 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) {
return filesWritten, nil
}

func checkOverwrite(force bool, paths ...string) error {
func checkOverwrite(writer ConfigWriter, force bool, paths ...string) error {
var existingFiles []string
// Check if the destination file exists.
for _, path := range paths {
_, err := os.Stat(path)
_, err := writer.Stat(path)
if os.IsNotExist(err) {
// File doesn't exist, proceed.
continue
Expand Down
17 changes: 12 additions & 5 deletions tool/tbot/botfs/botfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ const (
ACLRequired ACLMode = "required"
)

// OpenMode is a mode for opening files.
type OpenMode int

const (
// DefaultMode is the preferred permissions mode for bot files.
DefaultMode fs.FileMode = 0600
Expand All @@ -77,9 +80,13 @@ const (
// contents to succeed.
DefaultDirMode fs.FileMode = 0700

// OpenMode is the mode with which files should be opened for reading and
// ReadMode is the mode with which files should be opened for reading and
// writing.
OpenMode int = os.O_CREATE | os.O_RDWR
ReadMode OpenMode = OpenMode(os.O_CREATE | os.O_RDONLY)

// WriteMode is the mode with which files should be opened specifically
// for writing.
WriteMode OpenMode = OpenMode(os.O_CREATE | os.O_WRONLY | os.O_TRUNC)
)

// ACLOptions contains parameters needed to configure ACLs
Expand All @@ -94,8 +101,8 @@ type ACLOptions struct {

// openStandard attempts to open the given path for reading and writing with
// O_CREATE set.
func openStandard(path string) (*os.File, error) {
file, err := os.OpenFile(path, OpenMode, DefaultMode)
func openStandard(path string, mode OpenMode) (*os.File, error) {
file, err := os.OpenFile(path, int(mode), DefaultMode)
if err != nil {
return nil, trace.ConvertSystemError(err)
}
Expand All @@ -114,7 +121,7 @@ func createStandard(path string, isDir bool) error {
return nil
}

f, err := openStandard(path)
f, err := openStandard(path, WriteMode)
if err != nil {
return trace.Wrap(err)
}
Expand Down
22 changes: 11 additions & 11 deletions tool/tbot/botfs/fs_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ var missingSyscallWarning sync.Once

// openSecure opens the given path for writing (with O_CREAT, mode 0600)
// with the RESOLVE_NO_SYMLINKS flag set.
func openSecure(path string) (*os.File, error) {
func openSecure(path string, mode OpenMode) (*os.File, error) {
how := unix.OpenHow{
// Equivalent to 0600. Unfortunately it's not worth reusing our
// default file mode constant here.
Mode: unix.O_RDONLY | unix.S_IRUSR | unix.S_IWUSR,
Flags: uint64(OpenMode),
Flags: uint64(mode),
Resolve: unix.RESOLVE_NO_SYMLINKS,
}

Expand All @@ -78,16 +78,16 @@ func openSecure(path string) (*os.File, error) {
return os.NewFile(uintptr(fd), filepath.Base(path)), nil
}

// openSymlinks mode opens the file for read/write using the given symlink
// openSymlinks mode opens the file for read or write using the given symlink
// mode, potentially failing or logging a warning if symlinks can't be
// secured.
func openSymlinksMode(path string, symlinksMode SymlinksMode) (*os.File, error) {
func openSymlinksMode(path string, mode OpenMode, symlinksMode SymlinksMode) (*os.File, error) {
var file *os.File
var err error

switch symlinksMode {
case SymlinksSecure:
file, err = openSecure(path)
file, err = openSecure(path, mode)
if err == unix.ENOSYS {
return nil, trace.Errorf("openSecure(%q) failed due to missing "+
"syscall; `symlinks: insecure` may be required for this "+
Expand All @@ -96,7 +96,7 @@ func openSymlinksMode(path string, symlinksMode SymlinksMode) (*os.File, error)
return nil, trace.Wrap(err)
}
case SymlinksTrySecure:
file, err = openSecure(path)
file, err = openSecure(path, mode)
if err == unix.ENOSYS {
missingSyscallWarning.Do(func() {
log.Warnf("Failed to write to %q securely due to missing "+
Expand All @@ -105,15 +105,15 @@ func openSymlinksMode(path string, symlinksMode SymlinksMode) (*os.File, error)
"warning.", path)
})

file, err = openStandard(path)
file, err = openStandard(path, mode)
if err != nil {
return nil, trace.Wrap(err)
}
} else if err != nil {
return nil, trace.Wrap(err)
}
case SymlinksInsecure:
file, err = openStandard(path)
file, err = openStandard(path, mode)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -139,7 +139,7 @@ func createSecure(path string, isDir bool) error {
return nil
}

f, err := openSecure(path)
f, err := openSecure(path, WriteMode)
if err == unix.ENOSYS {
// bubble up the original error for comparison
return err
Expand Down Expand Up @@ -207,7 +207,7 @@ func Create(path string, isDir bool, symlinksMode SymlinksMode) error {

// Read reads the contents of the given file into memory.
func Read(path string, symlinksMode SymlinksMode) ([]byte, error) {
file, err := openSymlinksMode(path, symlinksMode)
file, err := openSymlinksMode(path, ReadMode, symlinksMode)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -224,7 +224,7 @@ func Read(path string, symlinksMode SymlinksMode) ([]byte, error) {

// Write stores the given data to the file at the given path.
func Write(path string, data []byte, symlinksMode SymlinksMode) error {
file, err := openSymlinksMode(path, symlinksMode)
file, err := openSymlinksMode(path, WriteMode, symlinksMode)
if err != nil {
return trace.Wrap(err)
}
Expand Down
4 changes: 2 additions & 2 deletions tool/tbot/botfs/fs_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func Read(path string, symlinksMode SymlinksMode) ([]byte, error) {
log.Warn("Secure symlinks not supported on this platform, set `symlinks: insecure` to disable this message", path)
}

file, err := openStandard(path)
file, err := openStandard(path, ReadMode)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -71,7 +71,7 @@ func Write(path string, data []byte, symlinksMode SymlinksMode) error {
log.Warn("Secure symlinks not supported on this platform, set `symlinks: insecure` to disable this message", path)
}

file, err := openStandard(path)
file, err := openStandard(path, WriteMode)
if err != nil {
return trace.Wrap(err)
}
Expand Down
Loading

0 comments on commit a880a39

Please sign in to comment.