Files
teleport/lib/service/service_test.go
T
STeve (Xin) Huang 3a0ab15f4a Support App access behind load balancer (#22565)
* Support App access behind load balancer

* revert unecessary change

* fix lint

* Fix UT

* minor refactor
2023-03-14 13:47:41 +00:00

1083 lines
30 KiB
Go

/*
Copyright 2015 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 service
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/coreos/go-semver/semver"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/breaker"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/lite"
"github.com/gravitational/teleport/lib/backend/memory"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
func TestMain(m *testing.M) {
utils.InitLoggerForTests()
os.Exit(m.Run())
}
func TestServiceSelfSignedHTTPS(t *testing.T) {
cfg := &servicecfg.Config{
DataDir: t.TempDir(),
Hostname: "example.com",
Log: utils.WrapLogger(logrus.New().WithField("test", "TestServiceSelfSignedHTTPS")),
}
require.NoError(t, initSelfSignedHTTPSCert(cfg))
require.Len(t, cfg.Proxy.KeyPairs, 1)
require.FileExists(t, cfg.Proxy.KeyPairs[0].Certificate)
require.FileExists(t, cfg.Proxy.KeyPairs[0].PrivateKey)
}
func TestMonitor(t *testing.T) {
t.Parallel()
fakeClock := clockwork.NewFakeClock()
cfg := servicecfg.MakeDefaultConfig()
cfg.Clock = fakeClock
var err error
cfg.DataDir = t.TempDir()
cfg.DiagnosticAddr = utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"}
cfg.SetAuthServerAddress(utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"})
cfg.Auth.Enabled = true
cfg.Auth.StorageConfig.Params["path"] = t.TempDir()
cfg.Auth.ListenAddr = utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"}
cfg.Proxy.Enabled = false
cfg.SSH.Enabled = false
cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig()
process, err := NewTeleport(cfg)
require.NoError(t, err)
// this simulates events that happened to be broadcast before the
// readyz.monitor started listening for events
process.BroadcastEvent(Event{Name: TeleportOKEvent, Payload: teleport.ComponentAuth})
require.NoError(t, process.Start())
t.Cleanup(func() { require.NoError(t, process.Close()) })
diagAddr, err := process.DiagnosticAddr()
require.NoError(t, err)
require.NotNil(t, diagAddr)
endpoint := fmt.Sprintf("http://%v/readyz", diagAddr.String())
waitForStatus := func(statusCodes ...int) func() bool {
return func() bool {
resp, err := http.Get(endpoint)
require.NoError(t, err)
resp.Body.Close()
for _, c := range statusCodes {
if resp.StatusCode == c {
return true
}
}
return false
}
}
require.Eventually(t, waitForStatus(http.StatusOK), 5*time.Second, 100*time.Millisecond)
tests := []struct {
desc string
event Event
advanceClock time.Duration
wantStatus []int
}{
{
desc: "degraded event causes degraded state",
event: Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentAuth},
wantStatus: []int{http.StatusServiceUnavailable, http.StatusBadRequest},
},
{
desc: "ok event causes recovering state",
event: Event{Name: TeleportOKEvent, Payload: teleport.ComponentAuth},
wantStatus: []int{http.StatusBadRequest},
},
{
desc: "ok event remains in recovering state because not enough time passed",
event: Event{Name: TeleportOKEvent, Payload: teleport.ComponentAuth},
wantStatus: []int{http.StatusBadRequest},
},
{
desc: "ok event after enough time causes OK state",
event: Event{Name: TeleportOKEvent, Payload: teleport.ComponentAuth},
advanceClock: defaults.HeartbeatCheckPeriod*2 + 1,
wantStatus: []int{http.StatusOK},
},
{
desc: "degraded event in a new component causes degraded state",
event: Event{Name: TeleportDegradedEvent, Payload: teleport.ComponentNode},
wantStatus: []int{http.StatusServiceUnavailable, http.StatusBadRequest},
},
{
desc: "ok event in one component keeps overall status degraded due to other component",
advanceClock: defaults.HeartbeatCheckPeriod*2 + 1,
event: Event{Name: TeleportOKEvent, Payload: teleport.ComponentAuth},
wantStatus: []int{http.StatusServiceUnavailable, http.StatusBadRequest},
},
{
desc: "ok event in new component causes overall recovering state",
advanceClock: defaults.HeartbeatCheckPeriod*2 + 1,
event: Event{Name: TeleportOKEvent, Payload: teleport.ComponentNode},
wantStatus: []int{http.StatusBadRequest},
},
{
desc: "ok event in new component causes overall OK state",
advanceClock: defaults.HeartbeatCheckPeriod*2 + 1,
event: Event{Name: TeleportOKEvent, Payload: teleport.ComponentNode},
wantStatus: []int{http.StatusOK},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
fakeClock.Advance(tt.advanceClock)
process.BroadcastEvent(tt.event)
require.Eventually(t, waitForStatus(tt.wantStatus...), 5*time.Second, 100*time.Millisecond)
})
}
}
// TestServiceCheckPrincipals checks certificates regeneration only requests
// regeneration when the principals change.
func TestServiceCheckPrincipals(t *testing.T) {
// Create a test auth server to extract the server identity (SSH and TLS
// certificates).
testAuthServer, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
tlsServer, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
defer tlsServer.Close()
testConnector := &Connector{
ServerIdentity: tlsServer.Identity,
}
tests := []struct {
inPrincipals []string
inDNS []string
outRegenerate bool
}{
// If nothing has been updated, don't regenerate certificate.
{
inPrincipals: []string{},
inDNS: []string{},
outRegenerate: false,
},
// Don't regenerate certificate if the node does not know it's own address.
{
inPrincipals: []string{"0.0.0.0"},
inDNS: []string{},
outRegenerate: false,
},
// If a new SSH principal is found, regenerate certificate.
{
inPrincipals: []string{"1.1.1.1"},
inDNS: []string{},
outRegenerate: true,
},
// If a new TLS DNS name is found, regenerate certificate.
{
inPrincipals: []string{},
inDNS: []string{"server.example.com"},
outRegenerate: true,
},
// Don't regenerate certificate if additional principals is already on the
// certificate.
{
inPrincipals: []string{"test-tls-server"},
inDNS: []string{},
outRegenerate: false,
},
}
for i, tt := range tests {
ok := checkServerIdentity(testConnector, tt.inPrincipals, tt.inDNS, logrus.New().WithField("test", "TestServiceCheckPrincipals"))
require.Equal(t, tt.outRegenerate, ok, "test %d", i)
}
}
// TestServiceInitExternalLog verifies that external logging can be used both as a means of
// overriding the local audit event target. Ideally, this test would also verify
// setup of true external loggers, but at the time of writing there isn't good
// support for setting up fake external logging endpoints.
func TestServiceInitExternalLog(t *testing.T) {
tts := []struct {
events []string
isNil bool
isErr bool
}{
// no URIs => no external logger
{isNil: true},
// local-only event uri w/o hostname => ok
{events: []string{"file:///tmp/teleport-test/events"}},
// local-only event uri w/ localhost => ok
{events: []string{"file://localhost/tmp/teleport-test/events"}},
// invalid host parameter => rejected
{events: []string{"file://example.com/should/fail"}, isErr: true},
// missing path specifier => rejected
{events: []string{"file://localhost"}, isErr: true},
}
for _, tt := range tts {
backend, err := memory.New(memory.Config{})
require.NoError(t, err)
t.Run(strings.Join(tt.events, ","), func(t *testing.T) {
// isErr implies isNil.
if tt.isErr {
tt.isNil = true
}
auditConfig, err := types.NewClusterAuditConfig(types.ClusterAuditConfigSpecV2{
AuditEventsURI: tt.events,
})
require.NoError(t, err)
loggers, err := initAuthExternalAuditLog(context.Background(), auditConfig, backend)
if tt.isErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if tt.isNil {
require.Nil(t, loggers)
} else {
require.NotNil(t, loggers)
}
})
}
}
func TestGetAdditionalPrincipals(t *testing.T) {
p := &TeleportProcess{
Config: &servicecfg.Config{
Hostname: "global-hostname",
HostUUID: "global-uuid",
AdvertiseIP: "1.2.3.4",
Proxy: servicecfg.ProxyConfig{
PublicAddrs: utils.MustParseAddrList("proxy-public-1", "proxy-public-2"),
SSHPublicAddrs: utils.MustParseAddrList("proxy-ssh-public-1", "proxy-ssh-public-2"),
TunnelPublicAddrs: utils.MustParseAddrList("proxy-tunnel-public-1", "proxy-tunnel-public-2"),
PostgresPublicAddrs: utils.MustParseAddrList("proxy-postgres-public-1", "proxy-postgres-public-2"),
MySQLPublicAddrs: utils.MustParseAddrList("proxy-mysql-public-1", "proxy-mysql-public-2"),
Kube: servicecfg.KubeProxyConfig{
Enabled: true,
PublicAddrs: utils.MustParseAddrList("proxy-kube-public-1", "proxy-kube-public-2"),
},
WebAddr: *utils.MustParseAddr(":443"),
},
Auth: servicecfg.AuthConfig{
PublicAddrs: utils.MustParseAddrList("auth-public-1", "auth-public-2"),
},
SSH: servicecfg.SSHConfig{
PublicAddrs: utils.MustParseAddrList("node-public-1", "node-public-2"),
},
Kube: servicecfg.KubeConfig{
PublicAddrs: utils.MustParseAddrList("kube-public-1", "kube-public-2"),
},
},
}
tests := []struct {
role types.SystemRole
wantPrincipals []string
wantDNS []string
}{
{
role: types.RoleProxy,
wantPrincipals: []string{
"global-hostname",
"proxy-public-1",
"proxy-public-2",
defaults.BindIP,
string(teleport.PrincipalLocalhost),
string(teleport.PrincipalLoopbackV4),
string(teleport.PrincipalLoopbackV6),
reversetunnel.LocalKubernetes,
"proxy-ssh-public-1",
"proxy-ssh-public-2",
"proxy-tunnel-public-1",
"proxy-tunnel-public-2",
"proxy-postgres-public-1",
"proxy-postgres-public-2",
"proxy-mysql-public-1",
"proxy-mysql-public-2",
"proxy-kube-public-1",
"proxy-kube-public-2",
},
wantDNS: []string{
"*.teleport.cluster.local",
"teleport.cluster.local",
"*.proxy-public-1",
"*.proxy-public-2",
"*.proxy-kube-public-1",
"*.proxy-kube-public-2",
},
},
{
role: types.RoleAuth,
wantPrincipals: []string{
"global-hostname",
"auth-public-1",
"auth-public-2",
},
wantDNS: []string{
"*.teleport.cluster.local",
"teleport.cluster.local",
},
},
{
role: types.RoleAdmin,
wantPrincipals: []string{
"global-hostname",
"auth-public-1",
"auth-public-2",
},
wantDNS: []string{
"*.teleport.cluster.local",
"teleport.cluster.local",
},
},
{
role: types.RoleNode,
wantPrincipals: []string{
"global-hostname",
"global-uuid",
"node-public-1",
"node-public-2",
"1.2.3.4",
},
wantDNS: []string{},
},
{
role: types.RoleKube,
wantPrincipals: []string{
"global-hostname",
string(teleport.PrincipalLocalhost),
string(teleport.PrincipalLoopbackV4),
string(teleport.PrincipalLoopbackV6),
reversetunnel.LocalKubernetes,
"kube-public-1",
"kube-public-2",
},
wantDNS: []string{
"*.teleport.cluster.local",
"teleport.cluster.local",
},
},
{
role: types.RoleApp,
wantPrincipals: []string{
"global-hostname",
"global-uuid",
},
wantDNS: []string{
"*.teleport.cluster.local",
"teleport.cluster.local",
},
},
{
role: types.SystemRole("unknown"),
wantPrincipals: []string{
"global-hostname",
},
wantDNS: []string{},
},
}
for _, tt := range tests {
t.Run(tt.role.String(), func(t *testing.T) {
principals, dns, err := p.getAdditionalPrincipals(tt.role)
require.NoError(t, err)
require.Empty(t, cmp.Diff(principals, tt.wantPrincipals))
require.Empty(t, cmp.Diff(dns, tt.wantDNS, cmpopts.EquateEmpty()))
})
}
}
// TestDesktopAccessFIPS makes sure that Desktop Access can not be started in
// FIPS mode. Remove this test once Rust code has been updated to use
// BoringCrypto instead of OpenSSL.
func TestDesktopAccessFIPS(t *testing.T) {
t.Parallel()
// Create and configure a default Teleport configuration.
cfg := servicecfg.MakeDefaultConfig()
cfg.SetAuthServerAddress(utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"})
cfg.Clock = clockwork.NewFakeClock()
cfg.DataDir = t.TempDir()
cfg.Auth.Enabled = false
cfg.Proxy.Enabled = false
cfg.SSH.Enabled = false
// Enable FIPS mode and Desktop Access, this should fail.
cfg.FIPS = true
cfg.WindowsDesktop.Enabled = true
_, err := NewTeleport(cfg)
require.Error(t, err)
}
type mockAccessPoint struct {
auth.ProxyAccessPoint
}
type mockReverseTunnelServer struct {
reversetunnel.Server
}
func TestSetupProxyTLSConfig(t *testing.T) {
testCases := []struct {
name string
acmeEnabled bool
wantNextProtos []string
}{
{
name: "ACME enabled, teleport ALPN protocols should be appended",
acmeEnabled: true,
wantNextProtos: []string{
// Ensure http/1.1 has precedence over http2.
"http/1.1",
"h2",
"acme-tls/1",
"teleport-tcp-ping",
"teleport-postgres-ping",
"teleport-mysql-ping",
"teleport-mongodb-ping",
"teleport-redis-ping",
"teleport-sqlserver-ping",
"teleport-snowflake-ping",
"teleport-cassandra-ping",
"teleport-elasticsearch-ping",
"teleport-dynamodb-ping",
"teleport-proxy-ssh",
"teleport-reversetunnel",
"teleport-auth@",
"teleport-tcp",
"teleport-proxy-ssh-grpc",
"teleport-proxy-grpc",
"teleport-proxy-grpc-mtls",
"teleport-postgres",
"teleport-mysql",
"teleport-mongodb",
"teleport-redis",
"teleport-sqlserver",
"teleport-snowflake",
"teleport-cassandra",
"teleport-elasticsearch",
"teleport-dynamodb",
},
},
{
name: "ACME disabled",
acmeEnabled: false,
wantNextProtos: []string{
"teleport-tcp-ping",
"teleport-postgres-ping",
"teleport-mysql-ping",
"teleport-mongodb-ping",
"teleport-redis-ping",
"teleport-sqlserver-ping",
"teleport-snowflake-ping",
"teleport-cassandra-ping",
"teleport-elasticsearch-ping",
"teleport-dynamodb-ping",
// Ensure http/1.1 has precedence over http2.
"http/1.1",
"h2",
"teleport-proxy-ssh",
"teleport-reversetunnel",
"teleport-auth@",
"teleport-tcp",
"teleport-proxy-ssh-grpc",
"teleport-proxy-grpc",
"teleport-proxy-grpc-mtls",
"teleport-postgres",
"teleport-mysql",
"teleport-mongodb",
"teleport-redis",
"teleport-sqlserver",
"teleport-snowflake",
"teleport-cassandra",
"teleport-elasticsearch",
"teleport-dynamodb",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig()
cfg.Proxy.ACME.Enabled = tc.acmeEnabled
cfg.DataDir = t.TempDir()
cfg.Proxy.PublicAddrs = utils.MustParseAddrList("localhost")
process := TeleportProcess{
Config: cfg,
// Setting Supervisor so that `ExitContext` can be called.
Supervisor: NewSupervisor("process-id", cfg.Log),
}
conn := &Connector{
ServerIdentity: &auth.Identity{
Cert: &ssh.Certificate{
Permissions: ssh.Permissions{
Extensions: map[string]string{},
},
},
},
}
tls, err := process.setupProxyTLSConfig(
conn,
&mockReverseTunnelServer{},
&mockAccessPoint{},
"cluster",
)
require.NoError(t, err)
require.Equal(t, tc.wantNextProtos, tls.NextProtos)
})
}
}
func TestTeleportProcess_reconnectToAuth(t *testing.T) {
t.Parallel()
clock := clockwork.NewFakeClock()
// Create and configure a default Teleport configuration.
cfg := servicecfg.MakeDefaultConfig()
cfg.SetAuthServerAddress(utils.NetAddr{AddrNetwork: "tcp", Addr: "127.0.0.1:0"})
cfg.Clock = clock
cfg.DataDir = t.TempDir()
cfg.Auth.Enabled = false
cfg.Proxy.Enabled = false
cfg.SSH.Enabled = true
cfg.MaxRetryPeriod = defaults.MaxWatcherBackoff
cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig()
cfg.ConnectFailureC = make(chan time.Duration, 5)
process, err := NewTeleport(cfg)
require.NoError(t, err)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
c, err := process.reconnectToAuthService(types.RoleAdmin)
require.Equal(t, ErrTeleportExited, err)
require.Nil(t, c)
}()
step := cfg.MaxRetryPeriod / 5.0
for i := 0; i < 5; i++ {
// wait for connection to fail
select {
case duration := <-process.Config.ConnectFailureC:
stepMin := step * time.Duration(i) / 2
stepMax := step * time.Duration(i+1)
require.GreaterOrEqual(t, duration, stepMin)
require.LessOrEqual(t, duration, stepMax)
// wait for connection to get to retry.After
clock.BlockUntil(1)
// add some extra to the duration to ensure the retry occurs
clock.Advance(cfg.MaxRetryPeriod)
case <-time.After(time.Minute):
t.Fatalf("timeout waiting for failure")
}
}
supervisor, ok := process.Supervisor.(*LocalSupervisor)
require.True(t, ok)
supervisor.signalExit()
wg.Wait()
}
func TestTeleportProcessAuthVersionCheck(t *testing.T) {
t.Parallel()
lib.SetInsecureDevMode(true)
defer lib.SetInsecureDevMode(false)
authAddr, err := getFreePort()
require.NoError(t, err)
listenAddr := utils.NetAddr{AddrNetwork: "tcp", Addr: authAddr}
token := "join-token"
// Create Node process.
nodeCfg := servicecfg.MakeDefaultConfig()
nodeCfg.SetAuthServerAddress(listenAddr)
nodeCfg.DataDir = t.TempDir()
nodeCfg.SetToken(token)
nodeCfg.Auth.Enabled = false
nodeCfg.Proxy.Enabled = false
nodeCfg.SSH.Enabled = true
// Set the Node's major version to be greater than the Auth Service's,
// which should make the version check fail.
currentVersion, err := semver.NewVersion(teleport.Version)
require.NoError(t, err)
currentVersion.Major++
nodeCfg.TeleportVersion = currentVersion.String()
// Create Auth Service process.
staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{
StaticTokens: []types.ProvisionTokenV1{
{
Roles: []types.SystemRole{
types.RoleNode,
},
Token: token,
},
},
})
require.NoError(t, err)
authCfg := servicecfg.MakeDefaultConfig()
authCfg.SetAuthServerAddress(listenAddr)
authCfg.DataDir = t.TempDir()
authCfg.Auth.Enabled = true
authCfg.Auth.StaticTokens = staticTokens
authCfg.Auth.StorageConfig.Type = lite.GetName()
authCfg.Auth.StorageConfig.Params = backend.Params{defaults.BackendPath: filepath.Join(authCfg.DataDir, defaults.BackendDir)}
authCfg.Auth.ListenAddr = listenAddr
authCfg.Proxy.Enabled = false
authCfg.SSH.Enabled = false
authProc, err := NewTeleport(authCfg)
require.NoError(t, err)
err = authProc.Start()
require.NoError(t, err)
t.Cleanup(func() {
authProc.Close()
})
t.Run("with version check", func(t *testing.T) {
testVersionCheck(t, nodeCfg, false)
})
t.Run("without version check", func(t *testing.T) {
testVersionCheck(t, nodeCfg, true)
})
}
func testVersionCheck(t *testing.T, nodeCfg *servicecfg.Config, skipVersionCheck bool) {
nodeCfg.SkipVersionCheck = skipVersionCheck
nodeProc, err := NewTeleport(nodeCfg)
require.NoError(t, err)
c, err := nodeProc.reconnectToAuthService(types.RoleNode)
if skipVersionCheck {
require.NoError(t, err)
require.NotNil(t, c)
} else {
require.True(t, trace.IsNotImplemented(err))
require.Nil(t, c)
}
supervisor, ok := nodeProc.Supervisor.(*LocalSupervisor)
require.True(t, ok)
supervisor.signalExit()
}
func getFreePort() (string, error) {
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return "", err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).String(), nil
}
func Test_readOrGenerateHostID(t *testing.T) {
var (
id = uuid.New().String()
hostUUIDKey = "/host_uuid"
)
type args struct {
kubeBackend *fakeKubeBackend
hostIDContent string
identity []*auth.Identity
}
tests := []struct {
name string
args args
wantFunc func(string) bool
wantKubeItemFunc func(*backend.Item) bool
}{
{
name: "load from storage without kube backend",
args: args{
kubeBackend: nil,
hostIDContent: id,
},
wantFunc: func(receivedID string) bool {
return receivedID == id
},
},
{
name: "Kube Backend is available but key is missing. Load from local storage and store in lube",
args: args{
kubeBackend: &fakeKubeBackend{
getData: nil,
getErr: fmt.Errorf("key not found"),
},
hostIDContent: id,
},
wantFunc: func(receivedID string) bool {
return receivedID == id
},
wantKubeItemFunc: func(i *backend.Item) bool {
return cmp.Diff(&backend.Item{
Key: []byte(hostUUIDKey),
Value: []byte(id),
}, i) == ""
},
},
{
name: "Kube Backend is available with key. Load from kube storage",
args: args{
kubeBackend: &fakeKubeBackend{
getData: &backend.Item{
Key: []byte(hostUUIDKey),
Value: []byte(id),
},
getErr: nil,
},
},
wantFunc: func(receivedID string) bool {
return receivedID == id
},
wantKubeItemFunc: func(i *backend.Item) bool {
return i == nil
},
},
{
name: "No hostID available. Generate one and store it into Kube and Local Storage",
args: args{
kubeBackend: &fakeKubeBackend{
getData: nil,
getErr: fmt.Errorf("key not found"),
},
},
wantFunc: func(receivedID string) bool {
_, err := uuid.Parse(receivedID)
return err == nil
},
wantKubeItemFunc: func(i *backend.Item) bool {
_, err := uuid.Parse(string(i.Value))
return err == nil && string(i.Key) == hostUUIDKey
},
},
{
name: "No hostID available. Generate one and store it into Local Storage",
args: args{
kubeBackend: nil,
},
wantFunc: func(receivedID string) bool {
_, err := uuid.Parse(receivedID)
return err == nil
},
wantKubeItemFunc: nil,
},
{
name: "No hostID available. Grab from provided static identity",
args: args{
kubeBackend: &fakeKubeBackend{
getData: nil,
getErr: fmt.Errorf("key not found"),
},
identity: []*auth.Identity{
{
ID: auth.IdentityID{
HostUUID: id,
},
},
},
},
wantFunc: func(receivedID string) bool {
return receivedID == id
},
wantKubeItemFunc: func(i *backend.Item) bool {
_, err := uuid.Parse(string(i.Value))
return err == nil && string(i.Key) == hostUUIDKey
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dataDir := t.TempDir()
// write host_uuid file to temp dir.
if len(tt.args.hostIDContent) > 0 {
err := utils.WriteHostUUID(dataDir, tt.args.hostIDContent)
require.NoError(t, err)
}
cfg := &servicecfg.Config{
DataDir: dataDir,
Log: logrus.New(),
JoinMethod: types.JoinMethodToken,
Identities: tt.args.identity,
}
var kubeBackend kubernetesBackend
if tt.args.kubeBackend != nil {
kubeBackend = tt.args.kubeBackend
}
err := readOrGenerateHostID(context.Background(), cfg, kubeBackend)
require.NoError(t, err)
require.True(t, tt.wantFunc(cfg.HostUUID))
if tt.args.kubeBackend != nil {
require.True(t, tt.wantKubeItemFunc(tt.args.kubeBackend.putData))
}
})
}
}
type fakeKubeBackend struct {
putData *backend.Item
getData *backend.Item
getErr error
}
// Put puts value into backend (creates if it does not
// exists, updates it otherwise)
func (f *fakeKubeBackend) Put(ctx context.Context, i backend.Item) (*backend.Lease, error) {
f.putData = &i
return &backend.Lease{}, nil
}
// Get returns a single item or not found error
func (f *fakeKubeBackend) Get(ctx context.Context, key []byte) (*backend.Item, error) {
return f.getData, f.getErr
}
func TestProxyGRPCServers(t *testing.T) {
hostID := uuid.NewString()
// Create a test auth server to extract the server identity (SSH and TLS
// certificates).
testAuthServer, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
Dir: t.TempDir(),
Clock: clockwork.NewFakeClockAt(time.Now()),
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, testAuthServer.Close())
})
tlsServer, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, tlsServer.Close())
})
// Create a new client using the server identity.
client, err := tlsServer.NewClient(auth.TestServerID(types.RoleProxy, hostID))
require.NoError(t, err)
// TLS config for proxy service.
serverIdentity, err := auth.NewServerIdentity(testAuthServer.AuthServer, hostID, types.RoleProxy)
require.NoError(t, err)
testConnector := &Connector{
ServerIdentity: serverIdentity,
Client: client,
}
// Create a listener for the insecure gRPC server.
insecureListener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() {
err := insecureListener.Close()
if errors.Is(err, net.ErrClosed) {
return
}
require.NoError(t, err)
})
// Create a listener for the secure gRPC server.
secureListener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() {
err := secureListener.Close()
if errors.Is(err, net.ErrClosed) {
return
}
require.NoError(t, err)
})
// Create a new Teleport process to initialize the gRPC servers with KubeProxy
// enabled.
log := logrus.New()
process := &TeleportProcess{
Supervisor: NewSupervisor(hostID, log),
Config: &servicecfg.Config{
Proxy: servicecfg.ProxyConfig{
Kube: servicecfg.KubeProxyConfig{
Enabled: true,
},
},
},
log: log,
}
// Create a limiter with no limits.
limiter, err := limiter.NewLimiter(limiter.Config{})
require.NoError(t, err)
// Create a error channel to collect the errors from the gRPC servers.
errC := make(chan error, 2)
t.Cleanup(func() {
for i := 0; i < 2; i++ {
err := <-errC
if errors.Is(err, net.ErrClosed) {
continue
}
require.NoError(t, err)
}
})
// Insecure gRPC server.
insecureGPRC := process.initPublicGRPCServer(limiter, testConnector, insecureListener)
t.Cleanup(insecureGPRC.GracefulStop)
proxyLockWatcher, err := services.NewLockWatcher(context.Background(), services.LockWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: teleport.ComponentProxy,
Client: testConnector.Client,
},
})
require.NoError(t, err)
t.Cleanup(func() {
proxyLockWatcher.Close()
})
// Secure gRPC server.
secureGRPC, err := process.initSecureGRPCServer(initSecureGRPCServerCfg{
limiter: limiter,
conn: testConnector,
listener: secureListener,
accessPoint: testConnector.Client,
lockWatcher: proxyLockWatcher,
emitter: testConnector.Client,
})
require.NoError(t, err)
t.Cleanup(secureGRPC.GracefulStop)
// Start the gRPC servers.
go func() {
errC <- trace.Wrap(insecureGPRC.Serve(insecureListener))
}()
go func() {
errC <- secureGRPC.Serve(secureListener)
}()
tests := []struct {
name string
credentials credentials.TransportCredentials
listenerAddr string
assertErr require.ErrorAssertionFunc
}{
{
name: "insecure client to insecure server",
credentials: insecure.NewCredentials(),
listenerAddr: insecureListener.Addr().String(),
assertErr: require.NoError,
},
{
name: "insecure client to secure server with insecure skip verify",
credentials: credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}),
listenerAddr: secureListener.Addr().String(),
assertErr: require.Error,
},
{
name: "insecure client to secure server",
credentials: credentials.NewTLS(&tls.Config{}),
listenerAddr: secureListener.Addr().String(),
assertErr: require.Error,
},
{
name: "secure client to secure server",
credentials: func() credentials.TransportCredentials {
// Create a new client using the server identity.
creds, err := testConnector.ServerIdentity.TLSConfig(nil)
require.NoError(t, err)
return credentials.NewTLS(creds)
}(),
listenerAddr: secureListener.Addr().String(),
assertErr: require.NoError,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
t.Cleanup(cancel)
_, err := grpc.DialContext(
ctx,
tt.listenerAddr,
grpc.WithTransportCredentials(tt.credentials),
// This setting is required to return the connection error instead of
// wrapping it in a "context deadline exceeded" error.
// It also enforces the grpc.WithBlock() option.
grpc.WithReturnConnectionError(),
grpc.WithDisableRetry(),
grpc.FailOnNonTempDialError(true),
)
tt.assertErr(t, err)
})
}
}