Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v9] Add new config templates to tbot for databases and identity files (#11596) #12500

Merged
merged 3 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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