Files
teleport/lib/sshutils/server_test.go
T
306b6a9db0 Set Teleport version in SSH clients (#65004)
* Set Teleport version in SSH clients.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add more tests. Fix lint.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix import. Fix lint exclude.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix lint - come on!

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix race by making a defensive copy.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Use ErrorIs.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Deep clone client conf. Early return for err. Make consistent.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Return new conf if nil. Add trace.Wrap to missing.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add IsSSHFeatureSupported. Switch from api to api_test for test pack. Make prefix exported const.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Revert accidental commit.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Return sentinel error for older clients or non-teleport clients to improve downstream handling.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Clean up.

Co-authored-by: Chris Thach <chris.thach@protonmail.com>

* Apply suggestions from code review

Co-authored-by: Edoardo Spadolini <edoardo.spadolini@gmail.com>

* Remove defensive check.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Change from trace.BadParam to reg error.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Use strings.Cut and improve readability.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Use error struct.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add test that errors if ssh.ClientConfig changes.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Move to subpackage.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Use static assertion instead.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Return error if config is nil.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Allow clients to override client version via config.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix test because I forgot to update it.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Move assert to test file.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Remove error return val.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Relax parser to allow for more.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix lint.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* WIP take on a tracessh wrapper. Will fail CI.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add tests and polish new package.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Revert changes to tracessh. Add lint config.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Revert changes to use tracessh.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Delete extra s.

Co-authored-by: Chris Thach <chris.thach@protonmail.com>

* Apply suggestions from code review.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add IsEmpty helper methods.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* First pass of refactoring.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Second pass refactor.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Third pass refactor.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix nil panic.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix missing user value.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix missing User value.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Remove Get from name. Polish.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix incorrect signers for clients. Make changes consistent.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Allow ClientConfig to be empty in proxy config.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix eager signers regression in generateClientConfig.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add edge case for space after prefix.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix issues from code review.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Drop suffix. Update comments.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Update ClientConfig docs.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add warning about using the Teleport version.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Teleport version is mandatory.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Revert timeout to make Claude/Codex happy.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Remove alias in places that didn't need it. Revert lib/cloud/gcp/alloydb.go.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add wrapper for tracessh.Client.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Add tests for generate client config.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Drop config from field name to shorten.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

* Fix lint issues.

Signed-off-by: Chris Thach <chris.thach@goteleport.com>

---------

Signed-off-by: Chris Thach <chris.thach@goteleport.com>
Co-authored-by: Edoardo Spadolini <edoardo.spadolini@gmail.com>
2026-04-07 18:06:00 +00:00

375 lines
10 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package sshutils
import (
"bytes"
"context"
"fmt"
"os"
"sync/atomic"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
apissh "github.com/gravitational/teleport/api/ssh"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/cert"
"github.com/gravitational/teleport/lib/utils/log/logtest"
)
func TestMain(m *testing.M) {
logtest.InitLogger(testing.Verbose)
os.Exit(m.Run())
}
func TestStartStop(t *testing.T) {
t.Parallel()
_, signer, err := cert.CreateTestEd25519Certificate("foo", ssh.HostCert)
require.NoError(t, err)
called := false
fn := NewChanHandlerFunc(func(_ context.Context, _ *ConnectionContext, nch ssh.NewChannel) {
called = true
err := nch.Reject(ssh.Prohibited, "nothing to see here")
assert.NoError(t, err)
})
srv, err := NewServer(
"test",
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
fn,
StaticHostSigners(signer),
AuthMethods{PublicKey: pubkeyAuth(signer.PublicKey())},
)
require.NoError(t, err)
require.NoError(t, srv.Start())
// Wait for SSH server to successfully shutdown, fail if it does not within
// the timeout period.
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
srv.Wait(ctx)
require.NoError(t, ctx.Err())
})
clientConfig := apissh.ClientConfig{
User: "alice",
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{signer}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(signer.PublicKey()),
}
clt, err := apissh.Dial(t.Context(), "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(t.Context())
require.Error(t, err)
require.ErrorContains(t, err, "nothing to see here")
require.True(t, called)
require.NoError(t, srv.Close())
}
// TestShutdown tests graceul shutdown feature
func TestShutdown(t *testing.T) {
t.Parallel()
_, signer, err := cert.CreateTestEd25519Certificate("foo", ssh.HostCert)
require.NoError(t, err)
closeContext, cancel := context.WithCancel(t.Context())
fn := NewChanHandlerFunc(func(_ context.Context, ccx *ConnectionContext, nch ssh.NewChannel) {
ch, _, err := nch.Accept()
require.NoError(t, err)
defer ch.Close()
<-closeContext.Done()
ccx.ServerConn.Close()
})
srv, err := NewServer(
"test",
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
fn,
StaticHostSigners(signer),
AuthMethods{PublicKey: pubkeyAuth(signer.PublicKey())},
SetShutdownPollPeriod(10*time.Millisecond),
)
require.NoError(t, err)
require.NoError(t, srv.Start())
clientConfig := apissh.ClientConfig{
User: "alice",
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{signer}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(signer.PublicKey()),
}
clt, err := apissh.Dial(t.Context(), "tcp", srv.Addr(), clientConfig)
require.NoError(t, err)
defer clt.Close()
// call new session to initiate opening new channel
_, err = clt.NewSession(t.Context())
require.NoError(t, err)
// context will timeout because there is a connection around
ctx, ctxc := context.WithTimeout(t.Context(), 50*time.Millisecond)
defer ctxc()
require.True(t, trace.IsConnectionProblem(srv.Shutdown(ctx)))
// now shutdown will return
cancel()
ctx2, ctxc2 := context.WithTimeout(t.Context(), time.Second)
defer ctxc2()
require.NoError(t, srv.Shutdown(ctx2))
// shutdown is re-entrable
ctx3, ctxc3 := context.WithTimeout(t.Context(), time.Second)
defer ctxc3()
require.NoError(t, srv.Shutdown(ctx3))
}
func TestConfigureCiphers(t *testing.T) {
t.Parallel()
_, signer, err := cert.CreateTestEd25519Certificate("foo", ssh.HostCert)
require.NoError(t, err)
fn := NewChanHandlerFunc(func(_ context.Context, _ *ConnectionContext, nch ssh.NewChannel) {
err := nch.Reject(ssh.Prohibited, "nothing to see here")
assert.NoError(t, err)
})
// create a server that only speaks aes128-ctr
srv, err := NewServer(
"test",
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
fn,
StaticHostSigners(signer),
AuthMethods{PublicKey: pubkeyAuth(signer.PublicKey())},
SetCiphers([]string{"aes128-ctr"}),
)
require.NoError(t, err)
require.NoError(t, srv.Start())
// client only speaks aes256-ctr, should fail
cc := apissh.ClientConfig{
SSHConfig: ssh.Config{
Ciphers: []string{"aes256-ctr"},
},
User: "alice",
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{signer}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(signer.PublicKey()),
}
_, err = apissh.Dial(t.Context(), "tcp", srv.Addr(), cc)
require.Error(t, err, "cipher mismatch, should fail, got nil")
// client only speaks aes128-ctr, should succeed
cc = apissh.ClientConfig{
SSHConfig: ssh.Config{
Ciphers: []string{"aes128-ctr"},
},
User: "alice",
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{signer}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(signer.PublicKey()),
}
clt, err := apissh.Dial(t.Context(), "tcp", srv.Addr(), cc)
require.NoError(t, err)
defer clt.Close()
}
// TestHostSigner makes sure Teleport can not be started with a invalid host
// certificate. The main check is the certificate algorithms.
func TestHostSignerFIPS(t *testing.T) {
t.Parallel()
_, signer, err := cert.CreateTestRSACertificate("foo", ssh.HostCert)
require.NoError(t, err)
_, ellipticSigner, err := cert.CreateTestECDSACertificate("foo", ssh.HostCert)
require.NoError(t, err)
_, ed25519Signer, err := cert.CreateTestEd25519Certificate("foo", ssh.HostCert)
require.NoError(t, err)
fn := NewChanHandlerFunc(func(_ context.Context, _ *ConnectionContext, nch ssh.NewChannel) {
err := nch.Reject(ssh.Prohibited, "nothing to see here")
assert.NoError(t, err)
})
tests := []struct {
inSigner ssh.Signer
inFIPS bool
assert require.ErrorAssertionFunc
}{
// Ed25519 when in FIPS mode should fail.
{
inSigner: ed25519Signer,
inFIPS: true,
assert: require.Error,
},
// ECDSA when in FIPS mode is okay.
{
inSigner: ellipticSigner,
inFIPS: true,
assert: require.NoError,
},
// RSA when in FIPS mode is okay.
{
inSigner: signer,
inFIPS: true,
assert: require.NoError,
},
// Ed25519 when in not FIPS mode should succeed.
{
inSigner: ed25519Signer,
inFIPS: false,
assert: require.NoError,
},
// ECDSA when in not FIPS mode should succeed.
{
inSigner: ellipticSigner,
inFIPS: false,
assert: require.NoError,
},
// RSA when in not FIPS mode should succeed.
{
inSigner: signer,
inFIPS: false,
assert: require.NoError,
},
}
for _, tt := range tests {
_, err := NewServer(
"test",
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
fn,
StaticHostSigners(tt.inSigner),
AuthMethods{Password: pass("abcdef123456")},
SetCiphers([]string{"aes128-ctr"}),
SetFIPS(tt.inFIPS),
)
tt.assert(t, err)
}
}
func pass(need string) PasswordFunc {
return func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
if string(password) == need {
return &ssh.Permissions{
Extensions: map[string]string{
utils.ExtIntCertType: utils.ExtIntCertTypeUser,
},
}, nil
}
return nil, fmt.Errorf("passwords don't match")
}
}
func TestDynamicHostSigners(t *testing.T) {
t.Parallel()
certFoo, signerFoo, err := cert.CreateTestEd25519Certificate("foo", ssh.HostCert)
require.NoError(t, err)
certBar, signerBar, err := cert.CreateTestEd25519Certificate("bar", ssh.HostCert)
require.NoError(t, err)
var activeSigner atomic.Pointer[ssh.Signer]
activeSigner.Store(&signerFoo)
srv, err := NewServer(
"test",
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
NewChanHandlerFunc(func(_ context.Context, _ *ConnectionContext, nc ssh.NewChannel) {
err := nc.Reject(ssh.UnknownChannelType, ssh.UnknownChannelType.String())
assert.NoError(t, err)
}),
func() []ssh.Signer { return []ssh.Signer{*activeSigner.Load()} },
AuthMethods{NoClient: true},
SetShutdownPollPeriod(10*time.Millisecond),
)
require.NoError(t, err)
require.NoError(t, srv.Start())
t.Cleanup(func() { _ = srv.Close() })
dial := func(pub ssh.PublicKey) error {
clt, err := apissh.Dial(t.Context(), "tcp", srv.Addr(), apissh.ClientConfig{
User: "alice",
PublicKeyAuth: apissh.PublicKeyAuthConfig{
Signers: func() ([]ssh.Signer, error) {
return []ssh.Signer{signerFoo}, nil
},
},
HostKeyCallback: ssh.FixedHostKey(pub),
})
if clt != nil {
defer clt.Close()
}
return err
}
require.NoError(t, dial(certFoo))
require.ErrorContains(t, dial(certBar), "ssh: host key mismatch")
activeSigner.Store(&signerBar)
require.NoError(t, dial(certBar))
require.ErrorContains(t, dial(certFoo), "ssh: host key mismatch")
}
// pubkeyAuth returns a PublicKeyFunc that checks if the provided public key matches the expected one.
func pubkeyAuth(need ssh.PublicKey) PublicKeyFunc {
return func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if bytes.Equal(key.Marshal(), need.Marshal()) {
return &ssh.Permissions{
Extensions: map[string]string{
utils.ExtIntCertType: utils.ExtIntCertTypeUser,
},
}, nil
}
return nil, trace.BadParameter("public keys don't match")
}
}