Skip to content
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
11 changes: 9 additions & 2 deletions lib/vnet/admin_process_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ import (
)

func newNetworkStackConfig(tun tunDevice, clt *clientApplicationServiceClient) (*networkStackConfig, error) {
sshProvider := newSSHProvider(sshProviderConfig{clt: clt})
clock := clockwork.NewRealClock()
sshProvider, err := newSSHProvider(sshProviderConfig{
clt: clt,
clock: clock,
})
if err != nil {
return nil, trace.Wrap(err)
}
tcpHandlerResolver := newTCPHandlerResolver(&tcpHandlerResolverConfig{
clt: clt,
appProvider: newAppProvider(clt),
sshProvider: sshProvider,
clock: clockwork.NewRealClock(),
clock: clock,
})
ipv6Prefix, err := newIPv6Prefix()
if err != nil {
Expand Down
84 changes: 80 additions & 4 deletions lib/vnet/ssh_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ package vnet

import (
"context"
"crypto/rand"
"net"
"strings"

"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"

"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/cryptosuites"
)

// sshHandler handles incoming VNet SSH connections.
Expand Down Expand Up @@ -61,13 +67,83 @@ func (h *sshHandler) handleTCPConnectorWithTargetConn(
connector func() (net.Conn, error),
targetConn net.Conn,
) error {
// For now we accept the incoming TCP conn to indicate that the node exists,
// but SSH connection forwarding is not implemented yet so we immediately
// close it.
hostCert, err := h.newHostCert(ctx)
if err != nil {
return trace.Wrap(err)
}

localConn, err := connector()
if err != nil {
return trace.Wrap(err)
}
localConn.Close()
defer localConn.Close()

// For now we accept the incoming SSH connection but forwarding to the
// target is not implemented yet so we immediately close it.
serverConfig := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if !sshutils.KeysEqual(h.cfg.sshProvider.trustedUserPublicKey, key) {
return nil, trace.AccessDenied("SSH client public key is not trusted")
}
return nil, nil
},
}
serverConfig.AddHostKey(hostCert)
Comment thread
nklaassen marked this conversation as resolved.
serverConn, chans, reqs, err := ssh.NewServerConn(localConn, serverConfig)
if err != nil {
return trace.Wrap(err, "accepting incoming SSH connection")
}
// Immediately close the connection but make sure to drain the channels.
serverConn.Close()
go ssh.DiscardRequests(reqs)
go func() {
for newChan := range chans {
_ = newChan.Reject(0, "")
}
}()
target := h.cfg.target
log.DebugContext(ctx, "Accepted incoming SSH connection",
"profile", target.profile,
"cluster", target.cluster,
"host", target.host,
"user", serverConn.User(),
)
return trace.NotImplemented("VNet SSH connection forwarding is not yet implemented")
}

func (h *sshHandler) newHostCert(ctx context.Context) (ssh.Signer, error) {
// If the user typed "ssh host.com" or "ssh host.com." our DNS handler will
// only see the fully-qualified variant with the trailing "." but the SSH
// client treats them differently, we need both in the principals if we want
// the cert to be trusted in both cases.
validPrincipals := []string{
h.cfg.target.fqdn,
strings.TrimSuffix(h.cfg.target.fqdn, "."),
}
// We generate an ephemeral key for every connection, Ed25519 is fast and
// well supported.
hostKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.Ed25519)
if err != nil {
return nil, trace.Wrap(err, "generating SSH host key")
}
hostSigner, err := ssh.NewSignerFromSigner(hostKey)
if err != nil {
return nil, trace.Wrap(err)
}
cert := &ssh.Certificate{
Key: hostSigner.PublicKey(),
Serial: 1,
CertType: ssh.HostCert,
ValidPrincipals: validPrincipals,
// This cert will only ever be used to handle this one SSH connection,
// the private key is held only in memory, the issuing CA is regenerated
// every time this process restarts and will only be trusted on this one
// host. The expiry doesn't matter.
ValidBefore: ssh.CertTimeInfinity,
}
if err := cert.SignCert(rand.Reader, h.cfg.sshProvider.hostCASigner); err != nil {
return nil, trace.Wrap(err, "signing SSH host cert")
}
certSigner, err := ssh.NewCertSigner(cert, hostSigner)
return certSigner, trace.Wrap(err)
}
54 changes: 49 additions & 5 deletions lib/vnet/ssh_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,26 @@ import (
"strings"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"golang.org/x/crypto/ssh"

proxyclient "github.com/gravitational/teleport/api/client/proxy"
vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1"
"github.com/gravitational/teleport/lib/cryptosuites"
)

// sshProvider provides methods necessary for VNet SSH access.
type sshProvider struct {
cfg sshProviderConfig
// hostCASigner is the host CA key used internally in VNet to terminate
// connections from clients, it is not a Teleport CA used by any cluster.
hostCASigner ssh.Signer
trustedUserPublicKey ssh.PublicKey
}

type sshProviderConfig struct {
clt *clientApplicationServiceClient
clt *clientApplicationServiceClient
clock clockwork.Clock
// overrideNodeDialer can be used in tests to dial SSH nodes with the real
// TLS configuration but without setting up the proxy transport service.
overrideNodeDialer func(
Expand All @@ -45,12 +52,45 @@ type sshProviderConfig struct {
tlsConfig *tls.Config,
dialOpts *vnetv1.DialOptions,
) (net.Conn, error)
// hostCASigner can be used in tests to set a specific key for the SSH host CA.
hostCASigner ssh.Signer
// trustedUserPublicKey can be used in tests to set a specific trusted user
// SSH key.
trustedUserPublicKey ssh.PublicKey
}

func newSSHProvider(cfg sshProviderConfig) *sshProvider {
return &sshProvider{
cfg: cfg,
func newSSHProvider(cfg sshProviderConfig) (*sshProvider, error) {
hostCASigner := cfg.hostCASigner
if hostCASigner == nil {
// TODO(nklaassen): write host CA public key to $TELEPORT_HOME/vnet_known_hosts
hostKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.Ed25519)
if err != nil {
return nil, trace.Wrap(err)
}
hostCASigner, err = ssh.NewSignerFromSigner(hostKey)
if err != nil {
return nil, trace.Wrap(err)
}
}
trustedUserPublicKey := cfg.trustedUserPublicKey
if trustedUserPublicKey == nil {
// TODO(nklaassen): check if $TELEPORT_HOME/id_vnet.pub exists.
// If it does, read that file and trust it.
// If not, generate the keypair and write it to $TELEPORT_HOME/id_vnet.
userKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.Ed25519)
if err != nil {
return nil, trace.Wrap(err)
}
trustedUserPublicKey, err = ssh.NewPublicKey(userKey.Public())
if err != nil {
return nil, trace.Wrap(err)
}
}
return &sshProvider{
cfg: cfg,
hostCASigner: hostCASigner,
trustedUserPublicKey: trustedUserPublicKey,
}, nil
}

// dial dials the target SSH host.
Expand Down Expand Up @@ -142,7 +182,10 @@ func (p *sshProvider) userTLSConfig(
}

type dialTarget struct {
profile, cluster, host string
fqdn string
profile string
cluster string
host string
}

func computeDialTarget(matchedCluster *vnetv1.MatchedCluster, fqdn string) dialTarget {
Expand All @@ -155,6 +198,7 @@ func computeDialTarget(matchedCluster *vnetv1.MatchedCluster, fqdn string) dialT
}
targetHost = targetHost + ":0"
return dialTarget{
fqdn: fqdn,
profile: targetProfile,
cluster: targetCluster,
host: targetHost,
Expand Down
Loading
Loading