mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
- 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.
388 lines
13 KiB
Go
388 lines
13 KiB
Go
/*
|
|
Copyright 2016-2022 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 (
|
|
"context"
|
|
"crypto/x509/pkix"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/gravitational/teleport/api/profile"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/sshutils"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
type testAuthority struct {
|
|
keygen *testauthority.Keygen
|
|
tlsCA *tlsca.CertAuthority
|
|
trustedCerts auth.TrustedCerts
|
|
}
|
|
|
|
func newTestAuthority(t *testing.T) testAuthority {
|
|
tlsCA, trustedCerts, err := newSelfSignedCA(CAPriv, "localhost")
|
|
require.NoError(t, err)
|
|
|
|
return testAuthority{
|
|
keygen: testauthority.New(),
|
|
tlsCA: tlsCA,
|
|
trustedCerts: trustedCerts,
|
|
}
|
|
}
|
|
|
|
// makeSignedKey helper returns a new user key signed by CAPriv key.
|
|
func (s *testAuthority) makeSignedKey(t *testing.T, idx KeyIndex, makeExpired bool) *Key {
|
|
priv, err := s.keygen.GeneratePrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
allowedLogins := []string{idx.Username, "root"}
|
|
ttl := 20 * time.Minute
|
|
if makeExpired {
|
|
ttl = -ttl
|
|
}
|
|
|
|
// reuse the same RSA keys for SSH and TLS keys
|
|
clock := clockwork.NewRealClock()
|
|
identity := tlsca.Identity{
|
|
Username: idx.Username,
|
|
Groups: []string{"groups"},
|
|
}
|
|
subject, err := identity.Subject()
|
|
require.NoError(t, err)
|
|
tlsCert, err := s.tlsCA.GenerateCertificate(tlsca.CertificateRequest{
|
|
Clock: clock,
|
|
PublicKey: priv.Public(),
|
|
Subject: subject,
|
|
NotAfter: clock.Now().UTC().Add(ttl),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
caSigner, err := ssh.ParsePrivateKey(CAPriv)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := s.keygen.GenerateUserCert(services.UserCertParams{
|
|
CASigner: caSigner,
|
|
PublicUserKey: ssh.MarshalAuthorizedKey(priv.SSHPublicKey()),
|
|
Username: idx.Username,
|
|
AllowedLogins: allowedLogins,
|
|
TTL: ttl,
|
|
PermitAgentForwarding: false,
|
|
PermitPortForwarding: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
key := NewKey(priv)
|
|
key.KeyIndex = idx
|
|
key.PrivateKey = priv
|
|
key.Cert = cert
|
|
key.TLSCert = tlsCert
|
|
key.TrustedCerts = []auth.TrustedCerts{s.trustedCerts}
|
|
key.DBTLSCerts["example-db"] = tlsCert
|
|
return key
|
|
}
|
|
|
|
func newSelfSignedCA(privateKey []byte, cluster string) (*tlsca.CertAuthority, auth.TrustedCerts, error) {
|
|
priv, err := keys.ParsePrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, auth.TrustedCerts{}, trace.Wrap(err)
|
|
}
|
|
|
|
cert, err := tlsca.GenerateSelfSignedCAWithSigner(priv, pkix.Name{
|
|
CommonName: cluster,
|
|
Organization: []string{cluster},
|
|
}, nil, defaults.CATTL)
|
|
if err != nil {
|
|
return nil, auth.TrustedCerts{}, trace.Wrap(err)
|
|
}
|
|
ca, err := tlsca.FromCertAndSigner(cert, priv)
|
|
if err != nil {
|
|
return nil, auth.TrustedCerts{}, trace.Wrap(err)
|
|
}
|
|
sshPub, err := ssh.NewPublicKey(priv.Public())
|
|
if err != nil {
|
|
return nil, auth.TrustedCerts{}, trace.Wrap(err)
|
|
}
|
|
return ca, auth.TrustedCerts{
|
|
ClusterName: cluster,
|
|
TLSCertificates: [][]byte{cert},
|
|
AuthorizedKeys: [][]byte{ssh.MarshalAuthorizedKey(sshPub)},
|
|
}, nil
|
|
}
|
|
|
|
func newTestFSClientStore(t *testing.T) *Store {
|
|
fsClientStore := NewFSClientStore(t.TempDir())
|
|
return fsClientStore
|
|
}
|
|
|
|
func testEachClientStore(t *testing.T, testFunc func(t *testing.T, clientStore *Store)) {
|
|
t.Run("FS", func(t *testing.T) {
|
|
testFunc(t, newTestFSClientStore(t))
|
|
})
|
|
|
|
t.Run("Mem", func(t *testing.T) {
|
|
testFunc(t, NewMemClientStore())
|
|
})
|
|
}
|
|
|
|
func TestClientStore(t *testing.T) {
|
|
t.Parallel()
|
|
a := newTestAuthority(t)
|
|
|
|
testEachClientStore(t, func(t *testing.T, clientStore *Store) {
|
|
t.Parallel()
|
|
|
|
idx := KeyIndex{
|
|
ProxyHost: "proxy.example.com",
|
|
ClusterName: "root",
|
|
Username: "test-user",
|
|
}
|
|
key := a.makeSignedKey(t, idx, false)
|
|
|
|
// Add key should add the key and trusted certs to their respective stores.
|
|
err := clientStore.AddKey(key)
|
|
require.NoError(t, err)
|
|
|
|
// the key's trusted certs should be added to the trusted certs store.
|
|
retrievedTrustedCerts, err := clientStore.GetTrustedCerts(idx.ProxyHost)
|
|
require.NoError(t, err)
|
|
require.Equal(t, key.TrustedCerts, retrievedTrustedCerts)
|
|
|
|
// Getting the key from the key store should have no trusted certs.
|
|
retrievedKey, err := clientStore.KeyStore.GetKey(idx, WithAllCerts...)
|
|
require.NoError(t, err)
|
|
expectKey := key.Copy()
|
|
expectKey.TrustedCerts = nil
|
|
require.Equal(t, expectKey, retrievedKey)
|
|
|
|
// Getting the key from the client store should fill in the trusted certs.
|
|
retrievedKey, err = clientStore.GetKey(idx, WithAllCerts...)
|
|
require.NoError(t, err)
|
|
require.Equal(t, key, retrievedKey)
|
|
|
|
var profileDir string
|
|
if fs, ok := clientStore.KeyStore.(*FSKeyStore); ok {
|
|
profileDir = fs.KeyDir
|
|
}
|
|
|
|
// Create and save a corresponding profile for the key.
|
|
profile := &profile.Profile{
|
|
WebProxyAddr: idx.ProxyHost + ":3080",
|
|
SiteName: idx.ClusterName,
|
|
Username: idx.Username,
|
|
}
|
|
err = clientStore.SaveProfile(profile, true)
|
|
require.NoError(t, err)
|
|
expectStatus, err := profileStatusFromKey(key, profileOptions{
|
|
ProfileName: profile.Name(),
|
|
WebProxyAddr: profile.WebProxyAddr,
|
|
ProfileDir: profileDir,
|
|
Username: profile.Username,
|
|
SiteName: profile.SiteName,
|
|
KubeProxyAddr: profile.KubeProxyAddr,
|
|
IsVirtual: profileDir == "",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// ReadProfileStatus should prepare a *ProfileStatus using the saved
|
|
// profile and key together.
|
|
profileStatus, err := clientStore.ReadProfileStatus(profile.Name())
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectStatus, profileStatus)
|
|
|
|
// FullProfileStatus should return the current profile status, and any
|
|
// other available profiles' statuses.
|
|
otherKey := key.Copy()
|
|
otherKey.ProxyHost = "other.example.com"
|
|
err = clientStore.AddKey(otherKey)
|
|
require.NoError(t, err)
|
|
|
|
otherProfile := profile.Copy()
|
|
otherProfile.WebProxyAddr = "other.example.com:3080"
|
|
err = clientStore.SaveProfile(otherProfile, false)
|
|
require.NoError(t, err)
|
|
|
|
expectOtherStatus, err := profileStatusFromKey(key, profileOptions{
|
|
ProfileName: otherProfile.Name(),
|
|
WebProxyAddr: otherProfile.WebProxyAddr,
|
|
ProfileDir: profileDir,
|
|
Username: otherProfile.Username,
|
|
SiteName: otherProfile.SiteName,
|
|
KubeProxyAddr: otherProfile.KubeProxyAddr,
|
|
IsVirtual: profileDir == "",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
currentStatus, otherStatuses, err := clientStore.FullProfileStatus()
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectStatus, currentStatus)
|
|
require.Len(t, otherStatuses, 1)
|
|
require.Equal(t, expectOtherStatus, otherStatuses[0])
|
|
})
|
|
}
|
|
|
|
// TestProxySSHConfig tests proxy client SSH config function
|
|
// that generates SSH client configuration for proxy tunnel connections
|
|
func TestProxySSHConfig(t *testing.T) {
|
|
t.Parallel()
|
|
auth := newTestAuthority(t)
|
|
|
|
testEachClientStore(t, func(t *testing.T, clientStore *Store) {
|
|
t.Parallel()
|
|
|
|
idx := KeyIndex{"host.a", "bob", "root"}
|
|
key := auth.makeSignedKey(t, idx, false)
|
|
|
|
caPub, _, _, _, err := ssh.ParseAuthorizedKey(CAPub)
|
|
require.NoError(t, err)
|
|
|
|
err = clientStore.AddKey(key)
|
|
require.NoError(t, err)
|
|
|
|
firsthost := "127.0.0.1"
|
|
err = clientStore.AddTrustedHostKeys(idx.ProxyHost, firsthost, caPub)
|
|
require.NoError(t, err)
|
|
|
|
retrievedKey, err := clientStore.GetKey(idx, WithSSHCerts{})
|
|
require.NoError(t, err)
|
|
|
|
clientConfig, err := retrievedKey.ProxyClientSSHConfig(firsthost)
|
|
require.NoError(t, err)
|
|
|
|
var called atomic.Int32
|
|
handler := sshutils.NewChanHandlerFunc(func(_ context.Context, _ *sshutils.ConnectionContext, nch ssh.NewChannel) {
|
|
called.Add(1)
|
|
nch.Reject(ssh.Prohibited, "nothing to see here")
|
|
})
|
|
|
|
hostPriv, hostPub, err := auth.keygen.GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
|
|
caSigner, err := ssh.ParsePrivateKey(CAPriv)
|
|
require.NoError(t, err)
|
|
|
|
hostCert, err := auth.keygen.GenerateHostCert(services.HostCertParams{
|
|
CASigner: caSigner,
|
|
PublicHostKey: hostPub,
|
|
HostID: "127.0.0.1",
|
|
NodeName: "127.0.0.1",
|
|
ClusterName: "host-cluster-name",
|
|
Role: types.RoleNode,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
hostSigner, err := sshutils.NewSigner(hostPriv, hostCert)
|
|
require.NoError(t, err)
|
|
|
|
srv, err := sshutils.NewServer(
|
|
"test",
|
|
utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"},
|
|
handler,
|
|
[]ssh.Signer{hostSigner},
|
|
sshutils.AuthMethods{
|
|
PublicKey: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
|
certChecker := apisshutils.CertChecker{
|
|
CertChecker: ssh.CertChecker{
|
|
IsUserAuthority: func(cert ssh.PublicKey) bool {
|
|
// Makes sure that user presented key signed by or with trusted authority.
|
|
return apisshutils.KeysEqual(caPub, cert)
|
|
},
|
|
},
|
|
}
|
|
return certChecker.Authenticate(conn, key)
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, srv.Start())
|
|
defer srv.Close()
|
|
|
|
clt, err := ssh.Dial("tcp", srv.Addr(), clientConfig)
|
|
require.NoError(t, err)
|
|
defer clt.Close()
|
|
|
|
// Call new session to initiate opening new channel. This should get
|
|
// rejected and fail.
|
|
_, err = clt.NewSession()
|
|
require.Error(t, err)
|
|
require.Equal(t, int(called.Load()), 1)
|
|
|
|
_, spub, err := testauthority.New().GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
caPub22, _, _, _, err := ssh.ParseAuthorizedKey(spub)
|
|
require.NoError(t, err)
|
|
err = clientStore.AddTrustedHostKeys(idx.ProxyHost, "second-host", caPub22)
|
|
require.NoError(t, err)
|
|
|
|
// The ProxyClientSSHConfig should create configuration that validates server authority only based on
|
|
// second-host instead of all known hosts.
|
|
retrievedKey, err = clientStore.GetKey(idx, WithSSHCerts{})
|
|
require.NoError(t, err)
|
|
clientConfig, err = retrievedKey.ProxyClientSSHConfig("second-host")
|
|
require.NoError(t, err)
|
|
|
|
// ssh server cert doesn't match second-host user known host thus connection should fail.
|
|
_, err = ssh.Dial("tcp", srv.Addr(), clientConfig)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
var (
|
|
CAPriv = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEowIBAAKCAQEAwBgwn+vkjCcKEr2fbX1mLN555B9amVYfD/fUZBNbXKpHaqYn
|
|
lM2WlyRR+xCrU9H/X6xT+wKJs1tsxFbxdBc1RWJtaqz/VpQCjomOulBzwumzB5hT
|
|
pJfGblGjkPvpt1zwfmKdpBg0jxXUHHR4u4N6OX0dxd0ImRQ4W9QUtEqzgqToS5u4
|
|
iwpeg6i1SoAdHBaSeqYhK9+nGrrJBAl/HVSgvL9tGn/+cQqlOiQz0t61V20+oMBA
|
|
P+rOTIiwRXn98iMKFjzVW1HTL5Lwit3oJQX0Lrd/I6tN2De6TJxbbOOkF45V/P/k
|
|
nBzbxV0fpnhcvZMnQqg1qdUmNVi6VC1O5qIPiwIDAQABAoIBAEg0T4KtLnkn63dj
|
|
41tKeW+AKJ0A1BMy9fYQl7sOM5c/QhzqW5JpPKOPOWl/uIaHNtCFfAOrzoqmYNnk
|
|
PFoApztvZeVlJY0rkVJ2jjmmJ/0pzuuZ7Ea/7gxlj2/d4NnVi2hWNR8LIiZudA5G
|
|
EWOaZgTZ7KkFDkhL+2s46pdiRNtj7l5FXn2tCh7jmFgKS4m1/QqV9KdE5EjwB2mj
|
|
BoP/j4V8O0RM05QpiYX/D5/Rr06tBavwTGW3vz/7OPIbf1el1mjfbLlt3z2tH0A5
|
|
BSGB4JEwIZ3+2xlZokHy95OSDzE46TsSzgNx3SDzGRc8UnSZN9yunxnL4ej11WYt
|
|
59YmD+ECgYEA3zxrDAtscpoxJSwcSkwqcMdElMK4D/BZw/tE9HhpHx3Pdd5XtMio
|
|
CHUkkqxwGJeVIixDjwnl4VfA1s0wy3CtHq6mmwfUviYrH2eqxe5RxNyZOZguk6is
|
|
GurZzD+ZfacsEIHyz2fZdnEAIFubu/S6x4TQPGg23oxnQpXXq1vzZFkCgYEA3Emz
|
|
W4MXvYWvRdbn+W3onHz/vty9owj/BKSP6giPGrpQFdLs8yoBUw1yTOGqAIfuWMLS
|
|
xvjULSlhei5PYD1xM2+B4luxM8K25DlqUpgRVtdmjQ/wxnzlmhDAPIMh7LUtw/6o
|
|
JJ+diAKTI86T8tokIL7WFaSvzdrz7/WrZQWkpoMCgYAPVAK1rQMhS10chE7c+yXe
|
|
4I/g9w3Ualh/kH1HnAz7yfw4x6+WBkEjc4ezWovH5ICk/A0XgUJ7mp7vIN+82FvK
|
|
w4tFEeCVveEwItojBR4wOkV7Iuvvz6EhqAaUc7mCWzw3VfTqMONJsrCjiCbFXSSG
|
|
FqSFwVIjLdjZRZitd37a4QKBgQDWfjjTIVlLY9EfWrszZu54+Ul4Sa2pAwh1N9sd
|
|
kUnuR33VUjUALGVvvgcOjyieLb1J1iGwNfc7JjDQ7CjD1+/Smn/IrWlksfKtVK6P
|
|
T5yKh2BGeEAEtPZHxom4IiM1PdEbJ2oHhxe3qHInCm2KqRdGfysrldjMw6aEfxxt
|
|
WEpTCwKBgHLZYgNf/dGgWgw7bVu/k61jxw3yZuU/0marFOPINME/AnTcSAGnkC0S
|
|
oDZhaPxjz3+2AHWAjUgW1ltTY8FsJYTOYsvzkYPfya4CgHCLg3D9ss1m4Rc7w5qo
|
|
Fa6bvW5jo543NztjlKts7XYVqroMCu0sIMS7R4JGsmw3VJcnnMP2
|
|
-----END RSA PRIVATE KEY-----`)
|
|
|
|
CAPub = []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAGDCf6+SMJwoSvZ9tfWYs3nnkH1qZVh8P99RkE1tcqkdqpieUzZaXJFH7EKtT0f9frFP7AomzW2zEVvF0FzVFYm1qrP9WlAKOiY66UHPC6bMHmFOkl8ZuUaOQ++m3XPB+Yp2kGDSPFdQcdHi7g3o5fR3F3QiZFDhb1BS0SrOCpOhLm7iLCl6DqLVKgB0cFpJ6piEr36causkECX8dVKC8v20af/5xCqU6JDPS3rVXbT6gwEA/6s5MiLBFef3yIwoWPNVbUdMvkvCK3eglBfQut38jq03YN7pMnFts46QXjlX8/+ScHNvFXR+meFy9kydCqDWp1SY1WLpULU7mog+L ekontsevoy@turing`)
|
|
)
|