Files
teleport/lib/client/keystore.go
T
Brian Joerger 488af75797 Client store generalization (#19420)
- Add a generalized client store made up of a key, profile, and trusted certs store. Each sub store can support different backends (~/.tsh, identity_file, in-memory). 
- Replace custom identity file handling with in-memory client store.
- Fix issues with trusted certs handling.
2023-01-06 01:34:04 +00:00

650 lines
20 KiB
Go

/*
Copyright 2016 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 client
import (
"os"
"path/filepath"
"runtime"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/profile"
"github.com/gravitational/teleport/api/utils/keypaths"
"github.com/gravitational/teleport/api/utils/keys"
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
)
const (
// profileDirPerms is the default permissions applied to the profile
// directory (usually ~/.tsh)
profileDirPerms os.FileMode = 0700
// keyFilePerms is the default permissions applied to key files (.cert, .key, pub)
// under ~/.tsh
keyFilePerms os.FileMode = 0600
// tshConfigFileName is the name of the directory containing the
// tsh config file.
tshConfigFileName = "config"
// tshAzureDirName is the name of the directory containing the
// az cli app-specific profiles.
tshAzureDirName = "azure"
)
// KeyStore is a storage interface for client session keys and certificates.
type KeyStore interface {
// AddKey adds the given key to the store.
AddKey(key *Key) error
// GetKey returns the user's key including the specified certs. The key's
// TrustedCerts will be nil and should be filled in using a TrustedCertsStore.
GetKey(idx KeyIndex, opts ...CertOption) (*Key, error)
// DeleteKey deletes the user's key with all its certs.
DeleteKey(idx KeyIndex) error
// DeleteUserCerts deletes only the specified certs of the user's key,
// keeping the private key intact.
DeleteUserCerts(idx KeyIndex, opts ...CertOption) error
// DeleteKeys removes all session keys.
DeleteKeys() error
// GetSSHCertificates gets all certificates signed for the given user and proxy,
// including certificates for trusted clusters.
GetSSHCertificates(proxyHost, username string) ([]*ssh.Certificate, error)
}
// FSKeyStore is an on-disk implementation of the KeyStore interface.
//
// The FS store uses the file layout outlined in `api/utils/keypaths.go`.
type FSKeyStore struct {
// log holds the structured logger.
log logrus.FieldLogger
// KeyDir is the directory where all keys are stored.
KeyDir string
}
// NewFSKeyStore initializes a new FSClientStore.
//
// If dirPath is empty, sets it to ~/.tsh.
func NewFSKeyStore(dirPath string) *FSKeyStore {
dirPath = profile.FullProfilePath(dirPath)
return &FSKeyStore{
log: logrus.WithField(trace.Component, teleport.ComponentKeyStore),
KeyDir: dirPath,
}
}
// userKeyPath returns the private key path for the given KeyIndex.
func (fs *FSKeyStore) userKeyPath(idx KeyIndex) string {
return keypaths.UserKeyPath(fs.KeyDir, idx.ProxyHost, idx.Username)
}
// tlsCertPath returns the TLS certificate path given KeyIndex.
func (fs *FSKeyStore) tlsCertPath(idx KeyIndex) string {
return keypaths.TLSCertPath(fs.KeyDir, idx.ProxyHost, idx.Username)
}
// sshDir returns the SSH certificate path for the given KeyIndex.
func (fs *FSKeyStore) sshDir(proxy, user string) string {
return keypaths.SSHDir(fs.KeyDir, proxy, user)
}
// sshCertPath returns the SSH certificate path for the given KeyIndex.
func (fs *FSKeyStore) sshCertPath(idx KeyIndex) string {
return keypaths.SSHCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName)
}
// ppkFilePath returns the PPK (PuTTY-formatted) keypair path for the given KeyIndex.
func (fs *FSKeyStore) ppkFilePath(idx KeyIndex) string {
return keypaths.PPKFilePath(fs.KeyDir, idx.ProxyHost, idx.Username)
}
// publicKeyPath returns the public key path for the given KeyIndex.
func (fs *FSKeyStore) publicKeyPath(idx KeyIndex) string {
return keypaths.PublicKeyPath(fs.KeyDir, idx.ProxyHost, idx.Username)
}
// appCertPath returns the TLS certificate path for the given KeyIndex and app name.
func (fs *FSKeyStore) appCertPath(idx KeyIndex, appname string) string {
return keypaths.AppCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, appname)
}
// databaseCertPath returns the TLS certificate path for the given KeyIndex and database name.
func (fs *FSKeyStore) databaseCertPath(idx KeyIndex, dbname string) string {
return keypaths.DatabaseCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, dbname)
}
// kubeCertPath returns the TLS certificate path for the given KeyIndex and kube cluster name.
func (fs *FSKeyStore) kubeCertPath(idx KeyIndex, kubename string) string {
return keypaths.KubeCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, kubename)
}
// AddKey adds the given key to the store.
func (fs *FSKeyStore) AddKey(key *Key) error {
if err := key.KeyIndex.Check(); err != nil {
return trace.Wrap(err)
}
if err := fs.writeBytes(key.PrivateKeyPEM(), fs.userKeyPath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
}
if err := fs.writeBytes(key.MarshalSSHPublicKey(), fs.publicKeyPath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
}
// Store TLS cert
if err := fs.writeBytes(key.TLSCert, fs.tlsCertPath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
}
if runtime.GOOS == constants.WindowsOS {
ppkFile, err := key.PPKFile()
if err == nil {
if err := fs.writeBytes(ppkFile, fs.ppkFilePath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
}
} else if !trace.IsBadParameter(err) {
return trace.Wrap(err)
}
// PPKFile can only be generated from an RSA private key.
fs.log.WithError(err).Debugf("Cannot convert private key to PPK-formatted keypair.")
}
// Store per-cluster key data.
if len(key.Cert) > 0 {
if err := fs.writeBytes(key.Cert, fs.sshCertPath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
}
}
// TODO(awly): unit test this.
for kubeCluster, cert := range key.KubeTLSCerts {
// Prevent directory traversal via a crafted kubernetes cluster name.
//
// This will confuse cluster cert loading (GetKey will return
// kubernetes cluster names different from the ones stored here), but I
// don't expect any well-meaning user to create bad names.
kubeCluster = filepath.Clean(kubeCluster)
path := fs.kubeCertPath(key.KeyIndex, kubeCluster)
if err := fs.writeBytes(cert, path); err != nil {
return trace.Wrap(err)
}
}
for db, cert := range key.DBTLSCerts {
path := fs.databaseCertPath(key.KeyIndex, filepath.Clean(db))
if err := fs.writeBytes(cert, path); err != nil {
return trace.Wrap(err)
}
}
for app, cert := range key.AppTLSCerts {
path := fs.appCertPath(key.KeyIndex, filepath.Clean(app))
if err := fs.writeBytes(cert, path); err != nil {
return trace.Wrap(err)
}
}
return nil
}
func (fs *FSKeyStore) writeBytes(bytes []byte, fp string) error {
if err := os.MkdirAll(filepath.Dir(fp), os.ModeDir|profileDirPerms); err != nil {
return trace.ConvertSystemError(err)
}
err := os.WriteFile(fp, bytes, keyFilePerms)
return trace.ConvertSystemError(err)
}
// DeleteKey deletes the user's key with all its certs.
func (fs *FSKeyStore) DeleteKey(idx KeyIndex) error {
files := []string{
fs.userKeyPath(idx),
fs.publicKeyPath(idx),
fs.tlsCertPath(idx),
}
for _, fn := range files {
if err := os.Remove(fn); err != nil {
return trace.ConvertSystemError(err)
}
}
// we also need to delete the extra PuTTY-formatted .ppk file when running on Windows,
// but it may not exist when upgrading from v9 -> v10 and logging into an existing cluster.
// as such, deletion should be best-effort and not generate an error if it fails.
if runtime.GOOS == constants.WindowsOS {
os.Remove(fs.ppkFilePath(idx))
}
// Clear ClusterName to delete the user certs stored for all clusters.
idx.ClusterName = ""
return fs.DeleteUserCerts(idx, WithAllCerts...)
}
// DeleteUserCerts deletes only the specified certs of the user's key,
// keeping the private key intact.
// Empty clusterName indicates to delete the certs for all clusters.
//
// Useful when needing to log out of a specific service, like a particular
// database proxy.
func (fs *FSKeyStore) DeleteUserCerts(idx KeyIndex, opts ...CertOption) error {
for _, o := range opts {
certPath := o.certPath(fs.KeyDir, idx)
if err := os.RemoveAll(certPath); err != nil {
return trace.ConvertSystemError(err)
}
}
return nil
}
// DeleteKeys removes all session keys.
func (fs *FSKeyStore) DeleteKeys() error {
files, err := os.ReadDir(fs.KeyDir)
if err != nil {
return trace.ConvertSystemError(err)
}
for _, file := range files {
// Don't delete 'config' and 'azure' directories.
// TODO: this is hackish and really shouldn't be needed, but fs.KeyDir is `~/.tsh` while it probably should be `~/.tsh/keys` instead.
if file.IsDir() && file.Name() == tshConfigFileName {
continue
}
if file.IsDir() && file.Name() == tshAzureDirName {
continue
}
if file.IsDir() {
err := os.RemoveAll(filepath.Join(fs.KeyDir, file.Name()))
if err != nil {
return trace.ConvertSystemError(err)
}
continue
}
err := os.Remove(filepath.Join(fs.KeyDir, file.Name()))
if err != nil {
return trace.ConvertSystemError(err)
}
}
return nil
}
// GetKey returns the user's key including the specified certs.
// If the key is not found, returns trace.NotFound error.
func (fs *FSKeyStore) GetKey(idx KeyIndex, opts ...CertOption) (*Key, error) {
if len(opts) > 0 {
if err := idx.Check(); err != nil {
return nil, trace.Wrap(err, "GetKey with CertOptions requires a fully specified KeyIndex")
}
}
if _, err := os.ReadDir(fs.KeyDir); err != nil && trace.IsNotFound(err) {
return nil, trace.Wrap(err, "no session keys for %+v", idx)
}
tlsCertFile := fs.tlsCertPath(idx)
tlsCert, err := os.ReadFile(tlsCertFile)
if err != nil {
return nil, trace.ConvertSystemError(err)
}
priv, err := keys.LoadKeyPair(fs.userKeyPath(idx), fs.publicKeyPath(idx))
if err != nil {
return nil, trace.ConvertSystemError(err)
}
key := NewKey(priv)
key.KeyIndex = idx
key.TLSCert = tlsCert
for _, o := range opts {
if err := fs.updateKeyWithCerts(o, key); err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
}
// Note, we may be returning expired certificates here, that is okay. If a
// certificate is expired, it's the responsibility of the TeleportClient to
// perform cleanup of the certificates and the profile.
return key, nil
}
func (fs *FSKeyStore) updateKeyWithCerts(o CertOption, key *Key) error {
certPath := o.certPath(fs.KeyDir, key.KeyIndex)
info, err := os.Stat(certPath)
if err != nil {
return trace.ConvertSystemError(err)
}
fs.log.Debugf("Reading certificates from path %q.", certPath)
if info.IsDir() {
certDataMap := map[string][]byte{}
certFiles, err := os.ReadDir(certPath)
if err != nil {
return trace.ConvertSystemError(err)
}
for _, certFile := range certFiles {
name := keypaths.TrimCertPathSuffix(certFile.Name())
if isCert := name != certFile.Name(); isCert {
data, err := os.ReadFile(filepath.Join(certPath, certFile.Name()))
if err != nil {
return trace.ConvertSystemError(err)
}
certDataMap[name] = data
}
}
return o.updateKeyWithMap(key, certDataMap)
}
certBytes, err := os.ReadFile(certPath)
if err != nil {
return trace.ConvertSystemError(err)
}
return o.updateKeyWithBytes(key, certBytes)
}
// GetSSHCertificates gets all certificates signed for the given user and proxy.
func (fs *FSKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Certificate, error) {
certDir := fs.sshDir(proxyHost, username)
certFiles, err := os.ReadDir(certDir)
if err != nil {
return nil, trace.Wrap(err)
}
sshCerts := make([]*ssh.Certificate, len(certFiles))
for i, certFile := range certFiles {
data, err := os.ReadFile(filepath.Join(certDir, certFile.Name()))
if err != nil {
return nil, trace.ConvertSystemError(err)
}
sshCerts[i], err = apisshutils.ParseCertificate(data)
if err != nil {
return nil, trace.Wrap(err)
}
}
return sshCerts, nil
}
// CertOption is an additional step to run when loading/deleting user certificates.
type CertOption interface {
// certPath returns a path to the cert (or to a dir holding the certs)
// within the given key dir. For use with FSLocalKeyStore.
certPath(keyDir string, idx KeyIndex) string
// updateKeyWithBytes adds the cert bytes to the key and performs related checks.
updateKeyWithBytes(key *Key, certBytes []byte) error
// updateKeyWithMap adds the cert data map to the key and performs related checks.
updateKeyWithMap(key *Key, certMap map[string][]byte) error
// deleteFromKey deletes the cert data from the key.
deleteFromKey(key *Key)
}
// WithAllCerts lists all known CertOptions.
var WithAllCerts = []CertOption{WithSSHCerts{}, WithKubeCerts{}, WithDBCerts{}, WithAppCerts{}}
// WithSSHCerts is a CertOption for handling SSH certificates.
type WithSSHCerts struct{}
func (o WithSSHCerts) certPath(keyDir string, idx KeyIndex) string {
if idx.ClusterName == "" {
return keypaths.SSHDir(keyDir, idx.ProxyHost, idx.Username)
}
return keypaths.SSHCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName)
}
func (o WithSSHCerts) updateKeyWithBytes(key *Key, certBytes []byte) error {
key.Cert = certBytes
return nil
}
func (o WithSSHCerts) updateKeyWithMap(key *Key, certMap map[string][]byte) error {
return trace.NotImplemented("WithSSHCerts does not implement updateKeyWithMap")
}
func (o WithSSHCerts) deleteFromKey(key *Key) {
key.Cert = nil
}
// WithKubeCerts is a CertOption for handling kubernetes certificates.
type WithKubeCerts struct{}
func (o WithKubeCerts) certPath(keyDir string, idx KeyIndex) string {
if idx.ClusterName == "" {
return keypaths.KubeDir(keyDir, idx.ProxyHost, idx.Username)
}
return keypaths.KubeCertDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName)
}
func (o WithKubeCerts) updateKeyWithBytes(key *Key, certBytes []byte) error {
return trace.NotImplemented("WithKubeCerts does not implement updateKeyWithBytes")
}
func (o WithKubeCerts) updateKeyWithMap(key *Key, certMap map[string][]byte) error {
key.KubeTLSCerts = certMap
return nil
}
func (o WithKubeCerts) deleteFromKey(key *Key) {
key.KubeTLSCerts = make(map[string][]byte)
}
// WithDBCerts is a CertOption for handling database access certificates.
type WithDBCerts struct {
dbName string
}
func (o WithDBCerts) certPath(keyDir string, idx KeyIndex) string {
if idx.ClusterName == "" {
return keypaths.DatabaseDir(keyDir, idx.ProxyHost, idx.Username)
}
if o.dbName == "" {
return keypaths.DatabaseCertDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName)
}
return keypaths.DatabaseCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName, o.dbName)
}
func (o WithDBCerts) updateKeyWithBytes(key *Key, certBytes []byte) error {
return trace.NotImplemented("WithDBCerts does not implement updateKeyWithBytes")
}
func (o WithDBCerts) updateKeyWithMap(key *Key, certMap map[string][]byte) error {
key.DBTLSCerts = certMap
return nil
}
func (o WithDBCerts) deleteFromKey(key *Key) {
key.DBTLSCerts = make(map[string][]byte)
}
// WithAppCerts is a CertOption for handling application access certificates.
type WithAppCerts struct {
appName string
}
func (o WithAppCerts) certPath(keyDir string, idx KeyIndex) string {
if idx.ClusterName == "" {
return keypaths.AppDir(keyDir, idx.ProxyHost, idx.Username)
}
if o.appName == "" {
return keypaths.AppCertDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName)
}
return keypaths.AppCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName, o.appName)
}
func (o WithAppCerts) updateKeyWithBytes(key *Key, certBytes []byte) error {
return trace.NotImplemented("WithAppCerts does not implement updateKeyWithBytes")
}
func (o WithAppCerts) updateKeyWithMap(key *Key, certMap map[string][]byte) error {
key.AppTLSCerts = certMap
return nil
}
func (o WithAppCerts) deleteFromKey(key *Key) {
key.AppTLSCerts = make(map[string][]byte)
}
type MemKeyStore struct {
// keys is a three-dimensional map indexed by [proxyHost][username][clusterName]
keys keyMap
}
// keyMap is a three-dimensional map indexed by [proxyHost][username][clusterName]
type keyMap map[string]map[string]map[string]*Key
func NewMemKeyStore() *MemKeyStore {
return &MemKeyStore{
keys: make(keyMap),
}
}
// AddKey writes a key to the underlying key store.
func (ms *MemKeyStore) AddKey(key *Key) error {
if err := key.KeyIndex.Check(); err != nil {
return trace.Wrap(err)
}
_, ok := ms.keys[key.ProxyHost]
if !ok {
ms.keys[key.ProxyHost] = map[string]map[string]*Key{}
}
_, ok = ms.keys[key.ProxyHost][key.Username]
if !ok {
ms.keys[key.ProxyHost][key.Username] = map[string]*Key{}
}
keyCopy := key.Copy()
// TrustedCA is stored separately in the Memory store so we wipe out
// the keys' trusted CA to prevent inconsistencies.
keyCopy.TrustedCerts = nil
ms.keys[key.ProxyHost][key.Username][key.ClusterName] = keyCopy
return nil
}
// GetKey returns the user's key including the specified certs.
func (ms *MemKeyStore) GetKey(idx KeyIndex, opts ...CertOption) (*Key, error) {
if len(opts) > 0 {
if err := idx.Check(); err != nil {
return nil, trace.Wrap(err, "GetKey with CertOptions requires a fully specified KeyIndex")
}
}
// If clusterName is not specified then the cluster-dependent fields
// are not considered relevant and we may simply return any key
// associated with any cluster name whatsoever.
var key *Key
if idx.ClusterName == "" {
for _, k := range ms.keys[idx.ProxyHost][idx.Username] {
key = k
break
}
} else {
if k, ok := ms.keys[idx.ProxyHost][idx.Username][idx.ClusterName]; ok {
key = k
}
}
if key == nil {
return nil, trace.NotFound("key for %+v not found", idx)
}
retKey := NewKey(key.PrivateKey)
retKey.KeyIndex = idx
retKey.TLSCert = key.TLSCert
for _, o := range opts {
switch o.(type) {
case WithSSHCerts:
retKey.Cert = key.Cert
case WithKubeCerts:
retKey.KubeTLSCerts = key.KubeTLSCerts
case WithDBCerts:
retKey.DBTLSCerts = key.DBTLSCerts
case WithAppCerts:
retKey.AppTLSCerts = key.AppTLSCerts
}
}
return retKey, nil
}
// DeleteKey deletes the user's key with all its certs.
func (ms *MemKeyStore) DeleteKey(idx KeyIndex) error {
if _, ok := ms.keys[idx.ProxyHost][idx.Username][idx.ClusterName]; !ok {
return trace.NotFound("key for %+v not found", idx)
}
delete(ms.keys[idx.ProxyHost], idx.Username)
return nil
}
// DeleteKeys removes all session keys.
func (ms *MemKeyStore) DeleteKeys() error {
ms.keys = make(keyMap)
return nil
}
// DeleteUserCerts deletes only the specified certs of the user's key,
// keeping the private key intact.
// Empty clusterName indicates to delete the certs for all clusters.
//
// Useful when needing to log out of a specific service, like a particular
// database proxy.
func (ms *MemKeyStore) DeleteUserCerts(idx KeyIndex, opts ...CertOption) error {
var keys []*Key
if idx.ClusterName != "" {
key, ok := ms.keys[idx.ProxyHost][idx.Username][idx.ClusterName]
if !ok {
return nil
}
keys = []*Key{key}
} else {
keys = make([]*Key, 0, len(ms.keys[idx.ProxyHost][idx.Username]))
for _, key := range ms.keys[idx.ProxyHost][idx.Username] {
keys = append(keys, key)
}
}
for _, key := range keys {
for _, o := range opts {
o.deleteFromKey(key)
}
}
return nil
}
// GetSSHCertificates gets all certificates signed for the given user and proxy.
func (ms *MemKeyStore) GetSSHCertificates(proxyHost, username string) ([]*ssh.Certificate, error) {
var sshCerts []*ssh.Certificate
for _, key := range ms.keys[proxyHost][username] {
sshCert, err := key.SSHCert()
if err != nil {
return nil, trace.Wrap(err)
}
sshCerts = append(sshCerts, sshCert)
}
return sshCerts, nil
}