Files
teleport/lib/config/configuration_test.go
T
rosstimothy 7541d6d7d0 Introduce logtest package (#56741)
Moves all test related logger initialization and creation to the
logtest package to reduce testing symbols in production code. The
existing helpers in lib/utils have been left in place until the
enterprise references can be converted.

Updates #51023.
2025-07-15 18:11:10 +00:00

5311 lines
146 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 config
import (
"bytes"
"crypto/x509/pkix"
"encoding/base64"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/installers"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/lite"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/log/logtest"
)
type testConfigFiles struct {
tempDir string
configFile string // good
configFileNoContent string // empty file
configFileBadContent string // garbage inside
configFileStatic string // file from a static YAML fixture
}
var testConfigs testConfigFiles
func writeTestConfigs() error {
var err error
testConfigs.tempDir, err = os.MkdirTemp("", "teleport-config")
if err != nil {
return err
}
// create a good config file fixture
testConfigs.configFile = filepath.Join(testConfigs.tempDir, "good-config.yaml")
if err = os.WriteFile(testConfigs.configFile, []byte(makeConfigFixture()), 0o660); err != nil {
return err
}
// create a static config file fixture
testConfigs.configFileStatic = filepath.Join(testConfigs.tempDir, "static-config.yaml")
if err = os.WriteFile(testConfigs.configFileStatic, []byte(StaticConfigString), 0o660); err != nil {
return err
}
// create an empty config file
testConfigs.configFileNoContent = filepath.Join(testConfigs.tempDir, "empty-config.yaml")
if err = os.WriteFile(testConfigs.configFileNoContent, []byte(""), 0o660); err != nil {
return err
}
// create a bad config file fixture
testConfigs.configFileBadContent = filepath.Join(testConfigs.tempDir, "bad-config.yaml")
return os.WriteFile(testConfigs.configFileBadContent, []byte("bad-data!"), 0o660)
}
func (tc testConfigFiles) cleanup() {
if tc.tempDir != "" {
os.RemoveAll(tc.tempDir)
}
}
func TestMain(m *testing.M) {
logtest.InitLogger(testing.Verbose)
if err := writeTestConfigs(); err != nil {
testConfigs.cleanup()
fmt.Println("failed writing test configs:", err)
os.Exit(1)
}
res := m.Run()
testConfigs.cleanup()
os.Exit(res)
}
func TestSampleConfig(t *testing.T) {
testCases := []struct {
name string
input SampleFlags
expectError bool
expectClusterName ClusterName
expectLicenseFile string
expectProxyPublicAddrs apiutils.Strings
expectProxyWebAddr string
expectProxyKeyPairs []KeyPair
}{
{
name: "ACMEEnabled",
input: SampleFlags{
ClusterName: "cookie.localhost",
ACMEEnabled: true,
ACMEEmail: "[email protected]",
LicensePath: "/tmp/license.pem",
},
expectClusterName: ClusterName("cookie.localhost"),
expectLicenseFile: "/tmp/license.pem",
expectProxyPublicAddrs: apiutils.Strings{"cookie.localhost:443"},
expectProxyWebAddr: "0.0.0.0:443",
},
{
name: "public and web addr",
input: SampleFlags{
PublicAddr: "tele.example.com:4422",
},
expectProxyPublicAddrs: apiutils.Strings{"tele.example.com:4422"},
expectProxyWebAddr: "0.0.0.0:4422",
},
{
name: "public and web addr with default port",
input: SampleFlags{
PublicAddr: "tele.example.com",
},
expectProxyPublicAddrs: apiutils.Strings{"tele.example.com:443"},
expectProxyWebAddr: "0.0.0.0:443",
},
{
name: "key file missing",
input: SampleFlags{
CertFile: "/var/lib/teleport/fullchain.pem",
},
expectError: true,
},
{
name: "load x509 key pair failed",
input: SampleFlags{
KeyFile: "/var/lib/teleport/key.pem",
CertFile: "/var/lib/teleport/fullchain.pem",
},
expectError: true,
},
{
name: "cluster name missing",
input: SampleFlags{
ACMEEnabled: true,
},
expectError: true,
},
{
name: "ACMEEnabled conflict with key file",
input: SampleFlags{
ClusterName: "cookie.localhost",
ACMEEnabled: true,
KeyFile: "/var/lib/teleport/privkey.pem",
CertFile: "/var/lib/teleport/fullchain.pem",
},
expectError: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
sfc, err := MakeSampleFileConfig(testCase.input)
if testCase.expectError {
require.Error(t, err)
require.Nil(t, sfc)
return
}
require.NoError(t, err)
require.NotNil(t, sfc)
fn := filepath.Join(t.TempDir(), "default-config.yaml")
err = os.WriteFile(fn, []byte(sfc.DebugDumpToYAML()), 0o660)
require.NoError(t, err)
// make sure it could be parsed:
fc, err := ReadFromFile(fn)
require.NoError(t, err)
// validate a couple of values:
require.Equal(t, defaults.DataDir, fc.Global.DataDir)
require.Equal(t, "INFO", fc.Logger.Severity)
require.Equal(t, testCase.expectClusterName, fc.Auth.ClusterName)
require.Equal(t, testCase.expectLicenseFile, fc.Auth.LicenseFile)
require.Equal(t, testCase.expectProxyWebAddr, fc.Proxy.WebAddr)
require.ElementsMatch(t, testCase.expectProxyPublicAddrs, fc.Proxy.PublicAddr)
require.ElementsMatch(t, testCase.expectProxyKeyPairs, fc.Proxy.KeyPairs)
require.False(t, lib.IsInsecureDevMode())
})
}
}
// TestBooleanParsing tests that types.Bool and *types.BoolOption are parsed
// properly
func TestBooleanParsing(t *testing.T) {
testCases := []struct {
s string
b bool
}{
{s: "true", b: true},
{s: "'true'", b: true},
{s: "yes", b: true},
{s: "'yes'", b: true},
{s: "'1'", b: true},
{s: "1", b: true},
{s: "no", b: false},
{s: "0", b: false},
}
for i, tc := range testCases {
msg := fmt.Sprintf("test case %v", i)
conf, err := ReadFromString(base64.StdEncoding.EncodeToString(fmt.Appendf(nil, `
teleport:
advertise_ip: 10.10.10.1
proxy_service:
enabled: yes
trust_x_forwarded_for: %v
auth_service:
enabled: yes
disconnect_expired_cert: %v
`, tc.s, tc.s)))
require.NoError(t, err, msg)
require.Equal(t, tc.b, conf.Proxy.TrustXForwardedFor.Value(), msg)
require.Equal(t, tc.b, conf.Auth.DisconnectExpiredCert.Value, msg)
}
}
// TestDurationParsing tests that duration options
// are parsed properly
func TestDuration(t *testing.T) {
testCases := []struct {
s string
d time.Duration
}{
{s: "1s", d: time.Second},
{s: "never", d: 0},
{s: "'1m'", d: time.Minute},
}
for i, tc := range testCases {
comment := fmt.Sprintf("test case %v", i)
conf, err := ReadFromString(base64.StdEncoding.EncodeToString(fmt.Appendf(nil, `
teleport:
advertise_ip: 10.10.10.1
auth_service:
enabled: yes
client_idle_timeout: %v
`, tc.s)))
require.NoError(t, err, comment)
require.Equal(t, tc.d, conf.Auth.ClientIdleTimeout.Value(), comment)
}
}
func TestConfigReading(t *testing.T) {
// non-existing file:
conf, err := ReadFromFile("/heaven/trees/apple.ymL")
require.Error(t, err)
require.Contains(t, err.Error(), "failed to open file")
require.Nil(t, conf)
// bad content:
_, err = ReadFromFile(testConfigs.configFileBadContent)
require.Error(t, err)
// empty config (must not fail)
conf, err = ReadFromFile(testConfigs.configFileNoContent)
require.NoError(t, err)
require.NotNil(t, conf)
require.True(t, conf.Auth.Enabled())
require.True(t, conf.Proxy.Enabled())
require.True(t, conf.SSH.Enabled())
require.False(t, conf.Kube.Enabled())
require.False(t, conf.Discovery.Enabled())
// good config
conf, err = ReadFromFile(testConfigs.configFile)
require.NoError(t, err)
require.Empty(t, cmp.Diff(conf, &FileConfig{
Version: defaults.TeleportConfigVersionV1,
Global: Global{
NodeName: NodeName,
AuthServers: []string{"auth0.server.example.org:3024", "auth1.server.example.org:3024"},
Limits: ConnectionLimits{
MaxConnections: 100,
MaxUsers: 5,
Rates: ConnectionRates,
},
Logger: Log{
Output: "stderr",
Severity: "INFO",
Format: LogFormat{
Output: "text",
},
},
Storage: backend.Config{
Type: "sqlite",
},
DataDir: "/path/to/data",
CAPin: apiutils.Strings([]string{"rsa256:123", "rsa256:456"}),
},
Auth: Auth{
Service: Service{
defaultEnabled: true,
EnabledFlag: "Yeah",
ListenAddress: "tcp://auth",
},
LicenseFile: "lic.pem",
DisconnectExpiredCert: types.NewBoolOption(true),
ClientIdleTimeout: types.Duration(17 * time.Second),
WebIdleTimeout: types.Duration(19 * time.Second),
RoutingStrategy: types.RoutingStrategy_MOST_RECENT,
ProxyPingInterval: types.Duration(10 * time.Second),
SSHDialTimeout: types.Duration(45 * time.Second),
},
SSH: SSH{
Service: Service{
defaultEnabled: true,
EnabledFlag: "true",
ListenAddress: "tcp://ssh",
},
Labels: Labels,
Commands: CommandLabels,
},
Discovery: Discovery{
Service: Service{
defaultEnabled: false,
EnabledFlag: "true",
ListenAddress: "",
},
AWSMatchers: []AWSMatcher{
{
Types: []string{"ec2"},
Regions: []string{"us-west-1", "us-east-1"},
Tags: map[string]apiutils.Strings{
"a": {"b"},
},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
{
Types: []string{"eks"},
Regions: []string{"us-west-1", "us-east-1"},
Tags: map[string]apiutils.Strings{
"a": {"b"},
},
Integration: "integration1",
KubeAppDiscovery: true,
},
},
AzureMatchers: []AzureMatcher{
{
Types: []string{"aks"},
Regions: []string{"uswest1"},
ResourceTags: map[string]apiutils.Strings{
"a": {"b"},
},
ResourceGroups: []string{"group1"},
Subscriptions: []string{"sub1"},
},
},
GCPMatchers: []GCPMatcher{
{
Types: []string{"gke"},
Locations: []string{"uswest1"},
Labels: map[string]apiutils.Strings{
"a": {"b"},
},
ProjectIDs: []string{"p1", "p2"},
},
},
},
Proxy: Proxy{
Service: Service{
defaultEnabled: true,
EnabledFlag: "yes",
ListenAddress: "tcp://proxy_ssh_addr",
},
KeyFile: "/etc/teleport/proxy.key",
CertFile: "/etc/teleport/proxy.crt",
KeyPairs: []KeyPair{
{
PrivateKey: "/etc/teleport/proxy.key",
Certificate: "/etc/teleport/proxy.crt",
},
},
WebAddr: "tcp://web_addr",
TunAddr: "reverse_tunnel_address:3311",
IdP: IdP{
SAMLIdP: SAMLIdP{
EnabledFlag: "true",
BaseURL: "https://test-url.com",
},
},
},
Kube: Kube{
Service: Service{
EnabledFlag: "yes",
ListenAddress: "tcp://kube",
},
KubeClusterName: "kube-cluster",
PublicAddr: apiutils.Strings([]string{"kube-host:1234"}),
ResourceMatchers: []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"*": {"*"}},
},
},
},
Apps: Apps{
Service: Service{
EnabledFlag: "yes",
},
Apps: []*App{
{
Name: "foo",
URI: "http://127.0.0.1:8080",
PublicAddr: "foo.example.com",
StaticLabels: Labels,
DynamicLabels: CommandLabels,
},
{
Name: "mcp-everything",
StaticLabels: Labels,
MCP: &MCP{
Command: "docker",
RunAsHostUser: "docker",
Args: []string{"run", "-i", "--rm", "mcp/everything"},
},
},
},
ResourceMatchers: []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{
"*": {"*"},
},
},
},
},
Databases: Databases{
Service: Service{
EnabledFlag: "yes",
},
Databases: []*Database{
{
Name: "postgres",
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
StaticLabels: Labels,
DynamicLabels: CommandLabels,
},
},
ResourceMatchers: []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{
"a": {"b"},
},
},
{
Labels: map[string]apiutils.Strings{
"c": {"d"},
},
AWS: ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBAccess",
ExternalID: "externalID123",
},
},
},
AWSMatchers: []AWSMatcher{
{
Types: []string{"rds"},
Regions: []string{"us-west-1", "us-east-1"},
Tags: map[string]apiutils.Strings{
"a": {"b"},
},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
{
Types: []string{"rds"},
Regions: []string{"us-central-1"},
Tags: map[string]apiutils.Strings{
"c": {"d"},
},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
},
},
AzureMatchers: []AzureMatcher{
{
Subscriptions: []string{"sub1", "sub2"},
ResourceGroups: []string{"rg1", "rg2"},
Types: []string{"mysql"},
Regions: []string{"eastus", "westus"},
ResourceTags: map[string]apiutils.Strings{
"a": {"b"},
},
},
{
Subscriptions: []string{"sub3", "sub4"},
ResourceGroups: []string{"rg3", "rg4"},
Types: []string{"postgres"},
Regions: []string{"centralus"},
ResourceTags: map[string]apiutils.Strings{
"c": {"d"},
},
},
{
Subscriptions: nil,
ResourceGroups: nil,
Types: []string{"mysql", "postgres"},
Regions: []string{"centralus"},
ResourceTags: map[string]apiutils.Strings{
"e": {"f"},
},
},
},
},
Metrics: Metrics{
Service: Service{
ListenAddress: "tcp://metrics",
EnabledFlag: "yes",
},
KeyPairs: []KeyPair{
{
PrivateKey: "/etc/teleport/proxy.key",
Certificate: "/etc/teleport/proxy.crt",
},
},
CACerts: []string{"/etc/teleport/ca.crt"},
GRPCServerLatency: true,
GRPCClientLatency: true,
},
Debug: DebugService{
Service: Service{
defaultEnabled: true,
EnabledFlag: "yes",
},
},
WindowsDesktop: WindowsDesktopService{
Service: Service{
EnabledFlag: "yes",
ListenAddress: "tcp://windows_desktop",
},
PublicAddr: apiutils.Strings([]string{"winsrv.example.com:3028", "no-port.winsrv.example.com"}),
ADHosts: apiutils.Strings([]string{"win.example.com:3389", "no-port.win.example.com"}),
},
Tracing: TracingService{
EnabledFlag: "yes",
ExporterURL: "https://localhost:4318",
KeyPairs: []KeyPair{
{
PrivateKey: "/etc/teleport/exporter.key",
Certificate: "/etc/teleport/exporter.crt",
},
},
CACerts: []string{"/etc/teleport/exporter.crt"},
SamplingRatePerMillion: 10,
},
}, cmp.AllowUnexported(Service{})))
require.True(t, conf.Auth.Configured())
require.True(t, conf.Auth.Enabled())
require.True(t, conf.Proxy.Configured())
require.True(t, conf.Proxy.Enabled())
require.True(t, conf.SSH.Configured())
require.True(t, conf.SSH.Enabled())
require.True(t, conf.Kube.Configured())
require.True(t, conf.Kube.Enabled())
require.True(t, conf.Apps.Configured())
require.True(t, conf.Apps.Enabled())
require.True(t, conf.Databases.Configured())
require.True(t, conf.Databases.Enabled())
require.True(t, conf.Metrics.Configured())
require.True(t, conf.Metrics.Enabled())
require.True(t, conf.Debug.Configured())
require.True(t, conf.Debug.Enabled())
require.True(t, conf.WindowsDesktop.Configured())
require.True(t, conf.WindowsDesktop.Enabled())
require.True(t, conf.Tracing.Enabled())
// good config from file
conf, err = ReadFromFile(testConfigs.configFileStatic)
require.NoError(t, err)
require.NotNil(t, conf)
checkStaticConfig(t, conf)
// good config from base64 encoded string
conf, err = ReadFromString(base64.StdEncoding.EncodeToString([]byte(StaticConfigString)))
require.NoError(t, err)
require.NotNil(t, conf)
checkStaticConfig(t, conf)
}
func TestLabelParsing(t *testing.T) {
var conf servicecfg.SSHConfig
var err error
// empty spec. no errors, no labels
err = parseLabelsApply("", &conf)
require.NoError(t, err)
require.Nil(t, conf.CmdLabels)
require.Nil(t, conf.Labels)
// simple static labels
err = parseLabelsApply(`key=value,more="much better"`, &conf)
require.NoError(t, err)
require.NotNil(t, conf.CmdLabels)
require.Empty(t, conf.CmdLabels)
require.Equal(t, map[string]string{
"key": "value",
"more": "much better",
}, conf.Labels)
// static labels + command labels
err = parseLabelsApply(`key=value,more="much better",arch=[5m2s:/bin/uname -m "p1 p2"]`, &conf)
require.NoError(t, err)
require.Equal(t, map[string]string{
"key": "value",
"more": "much better",
}, conf.Labels)
require.Equal(t, services.CommandLabels{
"arch": &types.CommandLabelV2{
Period: types.NewDuration(time.Minute*5 + time.Second*2),
Command: []string{"/bin/uname", "-m", `"p1 p2"`},
},
}, conf.CmdLabels)
}
// TestFileConfigCheck makes sure we don't start with invalid settings.
func TestFileConfigCheck(t *testing.T) {
tests := []struct {
desc string
inConfig string
outError bool
}{
{
desc: "all defaults, valid",
inConfig: `
teleport:
`,
},
{
desc: "invalid cipher, not valid",
inConfig: `
teleport:
ciphers:
- aes256-ctr
- fake-cipher
kex_algos:
- kexAlgoCurve25519SHA256
mac_algos:
- [email protected]
`,
outError: true,
},
{
desc: "proxy-peering, valid",
inConfig: `
proxy_service:
peer_listen_addr: peerhost:1234
peer_public_addr: peer.example:1234
`,
},
}
for _, tt := range tests {
comment := tt.desc
_, err := ReadConfig(bytes.NewBufferString(tt.inConfig))
if tt.outError {
require.Error(t, err, comment)
} else {
require.NoError(t, err, comment)
}
}
}
func TestApplyConfig(t *testing.T) {
tempDir := t.TempDir()
authTokenPath := filepath.Join(tempDir, "small-config-token")
err := os.WriteFile(authTokenPath, []byte("join-token"), 0o644)
require.NoError(t, err)
caPinPath := filepath.Join(tempDir, "small-config-ca-pin")
err = os.WriteFile(caPinPath, []byte("ca-pin-from-file1\nca-pin-from-file2"), 0o644)
require.NoError(t, err)
staticTokenPath := filepath.Join(tempDir, "small-config-static-tokens")
err = os.WriteFile(staticTokenPath, []byte("token-from-file1\ntoken-from-file2"), 0o644)
require.NoError(t, err)
pkcs11LibPath := filepath.Join(tempDir, "fake-pkcs11-lib.so")
err = os.WriteFile(pkcs11LibPath, []byte("fake-pkcs11-lib"), 0o644)
require.NoError(t, err)
oktaAPITokenPath := filepath.Join(tempDir, "okta-api-token")
err = os.WriteFile(oktaAPITokenPath, []byte("okta-api-token"), 0o644)
require.NoError(t, err)
conf, err := ReadConfig(bytes.NewBufferString(fmt.Sprintf(
SmallConfigString,
authTokenPath,
caPinPath,
staticTokenPath,
pkcs11LibPath,
oktaAPITokenPath,
)))
require.NoError(t, err)
require.NotNil(t, conf)
require.Equal(t, apiutils.Strings{"web3:443"}, conf.Proxy.PublicAddr)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
token, err := cfg.Token()
require.NoError(t, err)
require.Equal(t, "join-token", token)
require.Equal(t, types.ProvisionTokensFromStatic([]types.ProvisionTokenV1{
{
Token: "xxx",
Roles: types.SystemRoles([]types.SystemRole{"Proxy", "Node"}),
Expires: time.Unix(0, 0).UTC(),
},
{
Token: "token-from-file1",
Roles: types.SystemRoles([]types.SystemRole{"Node"}),
Expires: time.Unix(0, 0).UTC(),
},
{
Token: "token-from-file2",
Roles: types.SystemRoles([]types.SystemRole{"Node"}),
Expires: time.Unix(0, 0).UTC(),
},
{
Token: "yyy",
Roles: types.SystemRoles([]types.SystemRole{"Auth"}),
Expires: time.Unix(0, 0).UTC(),
},
}), cfg.Auth.StaticTokens.GetStaticTokens())
require.Equal(t, "magadan", cfg.Auth.ClusterName.GetClusterName())
require.True(t, cfg.Auth.Preference.GetAllowLocalAuth())
require.Equal(t, "10.10.10.1", cfg.AdvertiseIP)
tunnelStrategyType, err := cfg.Auth.NetworkingConfig.GetTunnelStrategyType()
require.NoError(t, err)
require.Equal(t, types.AgentMesh, tunnelStrategyType)
require.Equal(t, types.DefaultAgentMeshTunnelStrategy(), cfg.Auth.NetworkingConfig.GetAgentMeshTunnelStrategy())
require.Equal(t, types.OriginConfigFile, cfg.Auth.Preference.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.SessionRecordingConfig.Origin())
require.True(t, cfg.Proxy.Enabled)
require.Equal(t, "tcp://webhost:3080", cfg.Proxy.WebAddr.FullAddress())
require.Equal(t, "tcp://tunnelhost:1001", cfg.Proxy.ReverseTunnelListenAddr.FullAddress())
require.Equal(t, "tcp://webhost:3336", cfg.Proxy.MySQLAddr.FullAddress())
require.Equal(t, "tcp://webhost:27017", cfg.Proxy.MongoAddr.FullAddress())
require.Len(t, cfg.Proxy.PostgresPublicAddrs, 1)
require.Equal(t, "tcp://postgres.example:5432", cfg.Proxy.PostgresPublicAddrs[0].FullAddress())
require.Len(t, cfg.Proxy.MySQLPublicAddrs, 1)
require.Equal(t, "tcp://mysql.example:3306", cfg.Proxy.MySQLPublicAddrs[0].FullAddress())
require.Len(t, cfg.Proxy.MongoPublicAddrs, 1)
require.Equal(t, "tcp://mongo.example:27017", cfg.Proxy.MongoPublicAddrs[0].FullAddress())
require.Equal(t, "tcp://peerhost:1234", cfg.Proxy.PeerAddress.FullAddress())
require.Equal(t, "tcp://peer.example:1234", cfg.Proxy.PeerPublicAddr.FullAddress())
require.True(t, cfg.Proxy.IdP.SAMLIdP.Enabled)
require.Empty(t, cfg.Proxy.IdP.SAMLIdP.BaseURL)
require.Equal(t, "tcp://127.0.0.1:3000", cfg.DiagnosticAddr.FullAddress())
u2fCAFromFile, err := os.ReadFile("testdata/u2f_attestation_ca.pem")
require.NoError(t, err)
require.Empty(t, cmp.Diff(cfg.Auth.Preference, &types.AuthPreferenceV2{
Kind: types.KindClusterAuthPreference,
Version: types.V2,
Metadata: types.Metadata{
Name: "cluster-auth-preference",
Namespace: apidefaults.Namespace,
Labels: map[string]string{types.OriginLabel: types.OriginConfigFile},
},
Spec: types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorWebauthn,
Webauthn: &types.Webauthn{
RPID: "goteleport.com",
AttestationAllowedCAs: []string{
string(u2fCAFromFile),
`-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1
YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY
DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg
U2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2
cMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E
ilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq
joU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH
BVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf
wCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet
X02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0
1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s
XhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2
lLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d
bW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq
Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8
SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7
-----END CERTIFICATE-----
`,
},
},
AllowLocalAuth: types.NewBoolOption(true),
DisconnectExpiredCert: types.NewBoolOption(false),
LockingMode: constants.LockingModeBestEffort,
AllowPasswordless: types.NewBoolOption(true),
AllowHeadless: types.NewBoolOption(true),
DefaultSessionTTL: types.Duration(apidefaults.CertDuration),
IDP: &types.IdPOptions{
SAML: &types.IdPSAMLOptions{
Enabled: types.NewBoolOption(true),
},
},
Okta: &types.OktaOptions{},
},
}, protocmp.Transform()))
require.Equal(t, pkcs11LibPath, cfg.Auth.KeyStore.PKCS11.Path)
require.Equal(t, "example_token", cfg.Auth.KeyStore.PKCS11.TokenLabel)
require.Equal(t, 1, *cfg.Auth.KeyStore.PKCS11.SlotNumber)
require.Equal(t, "example_pin", cfg.Auth.KeyStore.PKCS11.PIN)
require.ElementsMatch(t, []string{"ca-pin-from-string", "ca-pin-from-file1", "ca-pin-from-file2"}, cfg.CAPins)
require.True(t, cfg.Databases.Enabled)
require.Empty(t, cmp.Diff(cfg.Databases.ResourceMatchers,
[]services.ResourceMatcher{
{
Labels: map[string]apiutils.Strings{
"*": {"*"},
},
AWS: services.ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBAccess",
ExternalID: "externalID123",
},
},
},
))
require.Empty(t, cmp.Diff(cfg.Databases.AzureMatchers,
[]types.AzureMatcher{
{
Subscriptions: []string{"sub1", "sub2"},
ResourceGroups: []string{"group1", "group2"},
Types: []string{"postgres", "mysql"},
Regions: []string{"eastus", "centralus"},
ResourceTags: map[string]apiutils.Strings{
"a": {"b"},
},
},
{
Subscriptions: nil,
ResourceGroups: nil,
Types: []string{"postgres", "mysql"},
Regions: []string{"westus"},
ResourceTags: map[string]apiutils.Strings{
"c": {"d"},
},
},
}))
require.Empty(t, cmp.Diff(cfg.Databases.AWSMatchers,
[]types.AWSMatcher{
{
Types: []string{"rds"},
Regions: []string{"us-west-1"},
AssumeRole: &types.AssumeRole{
RoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
},
}))
require.True(t, cfg.Kube.Enabled)
require.Empty(t, cmp.Diff(cfg.Kube.ResourceMatchers,
[]services.ResourceMatcher{
{
Labels: map[string]apiutils.Strings{
"*": {"*"},
},
},
},
))
require.Equal(t, "/tmp/kubeconfig", cfg.Kube.KubeconfigPath)
require.Empty(t, cmp.Diff(cfg.Kube.StaticLabels,
map[string]string{
"testKey": "testValue",
},
))
require.True(t, cfg.Discovery.Enabled)
require.Equal(t, []string{"eu-central-1"}, cfg.Discovery.AWSMatchers[0].Regions)
require.Equal(t, []string{"ec2"}, cfg.Discovery.AWSMatchers[0].Types)
require.Equal(t, "arn:aws:iam::123456789012:role/DBDiscoverer", cfg.Discovery.AWSMatchers[0].AssumeRole.RoleARN)
require.Equal(t, "externalID123", cfg.Discovery.AWSMatchers[0].AssumeRole.ExternalID)
require.Equal(t, &types.InstallerParams{
InstallTeleport: true,
JoinMethod: "iam",
JoinToken: types.IAMInviteTokenName,
ScriptName: "default-installer",
SSHDConfig: types.SSHDConfigPath,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_SCRIPT,
}, cfg.Discovery.AWSMatchers[0].Params)
require.True(t, cfg.Okta.Enabled)
require.Equal(t, "https://some-endpoint", cfg.Okta.APIEndpoint)
require.Equal(t, oktaAPITokenPath, cfg.Okta.APITokenPath)
require.Equal(t, time.Second*300, cfg.Okta.SyncPeriod)
require.True(t, cfg.Okta.SyncSettings.SyncAccessLists)
}
// TestApplyConfigNoneEnabled makes sure that if a section is not enabled,
// it's fields are not read in.
func TestApplyConfigNoneEnabled(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(NoServicesConfigString))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.False(t, cfg.Auth.Enabled)
require.Empty(t, cfg.Auth.PublicAddrs)
require.Equal(t, types.OriginDefaults, cfg.Auth.Preference.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.SessionRecordingConfig.Origin())
require.False(t, cfg.Proxy.Enabled)
require.Empty(t, cfg.Proxy.PublicAddrs)
require.False(t, cfg.SSH.Enabled)
require.Empty(t, cfg.SSH.PublicAddrs)
require.False(t, cfg.Apps.Enabled)
require.False(t, cfg.Databases.Enabled)
require.False(t, cfg.Metrics.Enabled)
require.True(t, cfg.DebugService.Enabled)
require.False(t, cfg.WindowsDesktop.Enabled)
require.Empty(t, cfg.Proxy.PostgresPublicAddrs)
require.Empty(t, cfg.Proxy.MySQLPublicAddrs)
}
// TestApplyConfigNoneEnabled makes sure that if the auth file configuration
// does not have `cluster_auth_preference`, `cluster_networking_config` and
// `session_recording` fields, then they all have the origin label as defaults.
func TestApplyDefaultAuthResources(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(DefaultAuthResourcesConfigString))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.True(t, cfg.Auth.Enabled)
require.Equal(t, "example.com", cfg.Auth.ClusterName.GetClusterName())
require.Equal(t, types.OriginDefaults, cfg.Auth.Preference.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.SessionRecordingConfig.Origin())
}
// TestApplyCustomAuthPreference makes sure that if the auth file configuration
// has a `cluster_auth_preference` field, then it will have the origin label as
// config-file.
func TestApplyCustomAuthPreference(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(CustomAuthPreferenceConfigString))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.True(t, cfg.Auth.Enabled)
require.Equal(t, "example.com", cfg.Auth.ClusterName.GetClusterName())
require.Equal(t, types.OriginConfigFile, cfg.Auth.Preference.Origin())
require.True(t, cfg.Auth.Preference.GetDisconnectExpiredCert())
require.Equal(t, types.OriginDefaults, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.SessionRecordingConfig.Origin())
}
// TestApplyCustomAuthPreferenceWithMOTD makes sure that if the auth file configuration
// has only the `message_of_the_day` `cluster_auth_preference` field, then it will have
// the origin label as defaults (instead of config-file).
func TestApplyCustomAuthPreferenceWithMOTD(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(AuthPreferenceConfigWithMOTDString))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.True(t, cfg.Auth.Enabled)
require.Equal(t, "example.com", cfg.Auth.ClusterName.GetClusterName())
require.Equal(t, types.OriginDefaults, cfg.Auth.Preference.Origin())
require.Equal(t, "welcome!", cfg.Auth.Preference.GetMessageOfTheDay())
require.Equal(t, types.OriginDefaults, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.SessionRecordingConfig.Origin())
}
// TestApplyCustomNetworkingConfig makes sure that if the auth file configuration
// has a `cluster_networking_config` field, then it will have the origin label as
// config-file.
func TestApplyCustomNetworkingConfig(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(CustomNetworkingConfigString))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.True(t, cfg.Auth.Enabled)
require.Equal(t, "example.com", cfg.Auth.ClusterName.GetClusterName())
require.Equal(t, types.OriginDefaults, cfg.Auth.Preference.Origin())
require.Equal(t, types.OriginConfigFile, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, 10*time.Second, cfg.Auth.NetworkingConfig.GetWebIdleTimeout())
require.Equal(t, types.OriginDefaults, cfg.Auth.SessionRecordingConfig.Origin())
}
// TestApplyCustomSessionRecordingConfig makes sure that if the auth file configuration
// has a `session_recording` field, then it will have the origin label as
// config-file.
func TestApplyCustomSessionRecordingConfig(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(CustomSessionRecordingConfigString))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.True(t, cfg.Auth.Enabled)
require.Equal(t, "example.com", cfg.Auth.ClusterName.GetClusterName())
require.Equal(t, types.OriginDefaults, cfg.Auth.Preference.Origin())
require.Equal(t, types.OriginDefaults, cfg.Auth.NetworkingConfig.Origin())
require.Equal(t, types.OriginConfigFile, cfg.Auth.SessionRecordingConfig.Origin())
require.True(t, cfg.Auth.SessionRecordingConfig.GetProxyChecksHostKeys())
}
// TestPostgresPublicAddr makes sure Postgres proxy public address default
// port logic works correctly.
func TestPostgresPublicAddr(t *testing.T) {
tests := []struct {
desc string
fc *FileConfig
out []string
}{
{
desc: "postgres public address with port set",
fc: &FileConfig{
Proxy: Proxy{
WebAddr: "0.0.0.0:8080",
PublicAddr: []string{"web.example.com:443"},
PostgresPublicAddr: []string{"postgres.example.com:5432"},
},
},
out: []string{"postgres.example.com:5432"},
},
{
desc: "when port not set, defaults to web proxy public port",
fc: &FileConfig{
Proxy: Proxy{
WebAddr: "0.0.0.0:8080",
PublicAddr: []string{"web.example.com:443"},
PostgresPublicAddr: []string{"postgres.example.com"},
},
},
out: []string{"postgres.example.com:443"},
},
{
desc: "when port and public addr not set, defaults to web proxy listen port",
fc: &FileConfig{
Proxy: Proxy{
WebAddr: "0.0.0.0:8080",
PostgresPublicAddr: []string{"postgres.example.com"},
},
},
out: []string{"postgres.example.com:8080"},
},
{
desc: "when port and listen/public addrs not set, defaults to web proxy default port",
fc: &FileConfig{
Proxy: Proxy{
PostgresPublicAddr: []string{"postgres.example.com"},
},
},
out: []string{net.JoinHostPort("postgres.example.com", strconv.Itoa(defaults.HTTPListenPort))},
},
{
desc: "when PostgresAddr is provided with port, the explicitly provided port should be use",
fc: &FileConfig{
Proxy: Proxy{
WebAddr: "0.0.0.0:8080",
PostgresAddr: "0.0.0.0:12345",
PostgresPublicAddr: []string{"postgres.example.com"},
},
},
out: []string{"postgres.example.com:12345"},
},
{
desc: "when PostgresAddr is provided without port, defaults PostgresPort should be used",
fc: &FileConfig{
Proxy: Proxy{
WebAddr: "0.0.0.0:8080",
PostgresAddr: "0.0.0.0",
PostgresPublicAddr: []string{"postgres.example.com"},
},
},
out: []string{net.JoinHostPort("postgres.example.com", strconv.Itoa(defaults.PostgresListenPort))},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err := applyProxyConfig(test.fc, cfg)
require.NoError(t, err)
require.EqualValues(t, test.out, utils.NetAddrsToStrings(cfg.Proxy.PostgresPublicAddrs))
})
}
}
// TestProxyPeeringPublicAddr makes sure the public address can only be
// set if the listen addr is set.
func TestProxyPeeringPublicAddr(t *testing.T) {
tests := []struct {
desc string
fc *FileConfig
wantErr bool
}{
{
desc: "full proxy peering config",
fc: &FileConfig{
Proxy: Proxy{
PeerAddr: "peerhost:1234",
PeerPublicAddr: "peer.example:5432",
},
},
wantErr: false,
},
{
desc: "no public proxy peering addr in config",
fc: &FileConfig{
Proxy: Proxy{
PeerAddr: "peerhost:1234",
},
},
wantErr: false,
},
{
desc: "no private proxy peering addr in config",
fc: &FileConfig{
Proxy: Proxy{
PeerPublicAddr: "peer.example:1234",
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err := applyProxyConfig(test.fc, cfg)
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestUIConfig_ShowResources(t *testing.T) {
tests := []struct {
desc string
fc *FileConfig
expected constants.ShowResources
wantErr bool
}{
{
desc: "show resources sets default value",
fc: &FileConfig{
Proxy: Proxy{
UI: &UIConfig{
ScrollbackLines: 1000,
},
},
},
expected: constants.ShowResourcesRequestable,
},
{
desc: "show resources respects config setting",
fc: &FileConfig{
Proxy: Proxy{
UI: &UIConfig{
ScrollbackLines: 1000,
ShowResources: constants.ShowResourcesaccessibleOnly,
},
},
},
expected: constants.ShowResourcesaccessibleOnly,
},
{
desc: "show resources fails with bad setting",
fc: &FileConfig{
Proxy: Proxy{
UI: &UIConfig{
ScrollbackLines: 1000,
ShowResources: "bad",
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err := applyProxyConfig(test.fc, cfg)
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, cfg.Proxy.UI.ShowResources)
}
})
}
}
func TestProxyMustJoinViaAuth(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err := ApplyFileConfig(&FileConfig{
Version: defaults.TeleportConfigVersionV3,
Proxy: Proxy{Service: Service{EnabledFlag: "yes"}},
Global: Global{ProxyServer: "proxy.example.com:3080"},
}, cfg)
require.True(t, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
}
func TestBackendDefaults(t *testing.T) {
read := func(val string) *servicecfg.Config {
// Default value is lite backend.
conf, err := ReadConfig(bytes.NewBufferString(val))
require.NoError(t, err)
require.NotNil(t, conf)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
return cfg
}
// Default value is lite backend.
cfg := read(`teleport:
data_dir: /var/lib/teleport
`)
require.Equal(t, cfg.Auth.StorageConfig.Type, lite.GetName())
require.Equal(t, cfg.Auth.StorageConfig.Params[defaults.BackendPath], filepath.Join("/var/lib/teleport", defaults.BackendDir))
// If no path is specified, the default is picked. In addition, internally
// dir gets converted into lite.
cfg = read(`teleport:
data_dir: /var/lib/teleport
storage:
type: dir
`)
require.Equal(t, cfg.Auth.StorageConfig.Type, lite.GetName())
require.Equal(t, cfg.Auth.StorageConfig.Params[defaults.BackendPath], filepath.Join("/var/lib/teleport", defaults.BackendDir))
// Support custom paths for dir/lite backends.
cfg = read(`teleport:
data_dir: /var/lib/teleport
storage:
type: dir
path: /var/lib/teleport/mybackend
`)
require.Equal(t, lite.GetName(), cfg.Auth.StorageConfig.Type)
require.Equal(t, "/var/lib/teleport/mybackend", cfg.Auth.StorageConfig.Params[defaults.BackendPath])
// Kubernetes proxy is disabled by default.
cfg = read(`teleport:
data_dir: /var/lib/teleport
`)
require.False(t, cfg.Proxy.Kube.Enabled)
}
func TestTunnelStrategy(t *testing.T) {
tests := []struct {
desc string
config string
readErr require.ErrorAssertionFunc
applyErr require.ErrorAssertionFunc
tunnelStrategy any
}{
{
desc: "Ensure default is used when no tunnel strategy is given",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
}, "\n"),
readErr: require.NoError,
applyErr: require.NoError,
tunnelStrategy: types.DefaultAgentMeshTunnelStrategy(),
},
{
desc: "Ensure default parameters are used for proxy peering strategy",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
" tunnel_strategy:",
" type: proxy_peering",
}, "\n"),
readErr: require.NoError,
applyErr: require.NoError,
tunnelStrategy: types.DefaultProxyPeeringTunnelStrategy(),
},
{
desc: "Ensure proxy peering strategy parameters are set",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
" tunnel_strategy:",
" type: proxy_peering",
" agent_connection_count: 2",
}, "\n"),
readErr: require.NoError,
applyErr: require.NoError,
tunnelStrategy: &types.ProxyPeeringTunnelStrategy{
AgentConnectionCount: 2,
},
},
{
desc: "Ensure tunnel strategy cannot take unknown parameters",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
" tunnel_strategy:",
" type: agent_mesh",
" agent_connection_count: 2",
}, "\n"),
readErr: require.Error,
applyErr: require.NoError,
tunnelStrategy: types.DefaultAgentMeshTunnelStrategy(),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(tc.config))
tc.readErr(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
tc.applyErr(t, err)
var actualStrategy any
if cfg.Auth.NetworkingConfig == nil {
} else if s := cfg.Auth.NetworkingConfig.GetAgentMeshTunnelStrategy(); s != nil {
actualStrategy = s
} else if s := cfg.Auth.NetworkingConfig.GetProxyPeeringTunnelStrategy(); s != nil {
actualStrategy = s
}
require.Equal(t, tc.tunnelStrategy, actualStrategy)
})
}
}
func TestParseCachePolicy(t *testing.T) {
tcs := []struct {
in *CachePolicy
out *servicecfg.CachePolicy
err error
}{
{in: &CachePolicy{EnabledFlag: "yes", TTL: "never"}, out: &servicecfg.CachePolicy{Enabled: true}},
{in: &CachePolicy{EnabledFlag: "true", TTL: "10h"}, out: &servicecfg.CachePolicy{Enabled: true}},
{in: &CachePolicy{Type: "whatever", EnabledFlag: "false", TTL: "10h"}, out: &servicecfg.CachePolicy{Enabled: false}},
{in: &CachePolicy{Type: "name", EnabledFlag: "yes", TTL: "never"}, out: &servicecfg.CachePolicy{Enabled: true}},
{in: &CachePolicy{EnabledFlag: "no"}, out: &servicecfg.CachePolicy{Enabled: false}},
}
for i, tc := range tcs {
comment := fmt.Sprintf("test case #%v", i)
out, err := tc.in.Parse()
if tc.err != nil {
require.IsType(t, tc.err, err, comment)
} else {
require.NoError(t, err, comment)
require.Equal(t, out, tc.out, comment)
}
}
}
func checkStaticConfig(t *testing.T, conf *FileConfig) {
require.Equal(t, "xxxyyy", conf.AuthToken)
require.Equal(t, "10.10.10.1:3022", conf.AdvertiseIP)
require.Equal(t, "/var/run/teleport.pid", conf.PIDFile)
require.Empty(t, cmp.Diff(conf.Limits, ConnectionLimits{
MaxConnections: 90,
MaxUsers: 91,
Rates: []ConnectionRate{
{Average: 70, Burst: 71, Period: time.Minute + time.Second},
{Average: 170, Burst: 171, Period: 10*time.Minute + 10*time.Second},
},
}))
// proxy_service section is missing.
require.False(t, conf.Proxy.Configured())
require.True(t, conf.Proxy.Enabled())
require.False(t, conf.Proxy.Disabled()) // Missing "proxy_service" does NOT mean it's been disabled
require.Empty(t, cmp.Diff(conf.Proxy, Proxy{
Service: Service{defaultEnabled: true},
}, cmp.AllowUnexported(Service{})))
// kubernetes_service section is missing.
require.False(t, conf.Kube.Configured())
require.False(t, conf.Kube.Enabled())
require.True(t, conf.Kube.Disabled())
require.Empty(t, cmp.Diff(conf.Kube, Kube{
Service: Service{defaultEnabled: false},
}, cmp.AllowUnexported(Service{})))
require.True(t, conf.SSH.Configured()) // "ssh_service" has been explicitly set to "no"
require.False(t, conf.SSH.Enabled())
require.True(t, conf.SSH.Disabled())
require.Empty(t, cmp.Diff(conf.SSH, SSH{
Service: Service{
defaultEnabled: true,
EnabledFlag: "no",
ListenAddress: "ssh:3025",
},
Labels: map[string]string{
"name": "mongoserver",
"role": "follower",
},
Commands: []CommandLabel{
{Name: "hostname", Command: []string{"/bin/hostname"}, Period: 10 * time.Millisecond},
{Name: "date", Command: []string{"/bin/date"}, Period: 20 * time.Millisecond},
},
PublicAddr: apiutils.Strings{"luna3:22"},
}, cmp.AllowUnexported(Service{})))
require.Empty(t, cmp.Diff(conf.Discovery, Discovery{AWSMatchers: nil}, cmp.AllowUnexported(Service{})))
require.True(t, conf.Auth.Configured())
require.True(t, conf.Auth.Enabled())
require.False(t, conf.Auth.Disabled())
require.Empty(t, cmp.Diff(conf.Auth, Auth{
Service: Service{
defaultEnabled: true,
EnabledFlag: "yes",
ListenAddress: "auth:3025",
},
ReverseTunnels: []ReverseTunnel{
{
DomainName: "tunnel.example.com",
Addresses: []string{"com-1", "com-2"},
},
{
DomainName: "tunnel.example.org",
Addresses: []string{"org-1"},
},
},
StaticTokens: StaticTokens{
"proxy,node:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"auth:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
PublicAddr: apiutils.Strings{
"auth.default.svc.cluster.local:3080",
},
ClientIdleTimeout: types.Duration(17 * time.Second),
DisconnectExpiredCert: types.NewBoolOption(true),
RoutingStrategy: types.RoutingStrategy_MOST_RECENT,
}, cmp.AllowUnexported(Service{})))
policy, err := conf.CachePolicy.Parse()
require.NoError(t, err)
require.True(t, policy.Enabled)
require.Equal(t, time.Minute*12, policy.MaxRetryPeriod)
}
var (
NodeName = "edsger.example.com"
AuthServers = []string{"auth0.server.example.org:3024", "auth1.server.example.org:3024"}
ConnectionRates = []ConnectionRate{
{
Period: time.Minute,
Average: 5,
Burst: 10,
},
{
Period: time.Minute * 10,
Average: 10,
Burst: 100,
},
}
Labels = map[string]string{
"name": "mongoserver",
"role": "follower",
}
CommandLabels = []CommandLabel{
{
Name: "os",
Command: []string{"uname", "-o"},
Period: time.Minute * 15,
},
{
Name: "hostname",
Command: []string{"/bin/hostname"},
Period: time.Millisecond * 10,
},
}
)
// makeConfigFixture returns a valid content for teleport.yaml file
func makeConfigFixture() string {
conf := FileConfig{}
// common config:
conf.NodeName = NodeName
conf.DataDir = "/path/to/data"
conf.AuthServers = AuthServers
conf.Limits.MaxConnections = 100
conf.Limits.MaxUsers = 5
conf.Limits.Rates = ConnectionRates
conf.Logger.Output = "stderr"
conf.Logger.Severity = "INFO"
conf.Logger.Format = LogFormat{Output: "text"}
conf.Storage.Type = "sqlite"
conf.CAPin = []string{"rsa256:123", "rsa256:456"}
// auth service:
conf.Auth.EnabledFlag = "Yeah"
conf.Auth.ListenAddress = "tcp://auth"
conf.Auth.LicenseFile = "lic.pem"
conf.Auth.ClientIdleTimeout = types.NewDuration(17 * time.Second)
conf.Auth.WebIdleTimeout = types.NewDuration(19 * time.Second)
conf.Auth.DisconnectExpiredCert = types.NewBoolOption(true)
conf.Auth.RoutingStrategy = types.RoutingStrategy_MOST_RECENT
conf.Auth.ProxyPingInterval = types.NewDuration(10 * time.Second)
conf.Auth.SSHDialTimeout = types.NewDuration(45 * time.Second)
// ssh service:
conf.SSH.EnabledFlag = "true"
conf.SSH.ListenAddress = "tcp://ssh"
conf.SSH.Labels = Labels
conf.SSH.Commands = CommandLabels
// discovery service
conf.Discovery.EnabledFlag = "true"
conf.Discovery.AWSMatchers = []AWSMatcher{
{
Types: []string{"ec2"},
Regions: []string{"us-west-1", "us-east-1"},
Tags: map[string]apiutils.Strings{"a": {"b"}},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
{
Types: []string{"eks"},
Regions: []string{"us-west-1", "us-east-1"},
Tags: map[string]apiutils.Strings{"a": {"b"}},
Integration: "integration1",
KubeAppDiscovery: true,
},
}
conf.Discovery.AzureMatchers = []AzureMatcher{
{
Types: []string{"aks"},
Regions: []string{"uswest1"},
ResourceTags: map[string]apiutils.Strings{
"a": {"b"},
},
ResourceGroups: []string{"group1"},
Subscriptions: []string{"sub1"},
},
}
conf.Discovery.GCPMatchers = []GCPMatcher{
{
Types: []string{"gke"},
Locations: []string{"uswest1"},
Labels: map[string]apiutils.Strings{
"a": {"b"},
},
ProjectIDs: []string{"p1", "p2"},
},
}
// proxy-service:
conf.Proxy.EnabledFlag = "yes"
conf.Proxy.ListenAddress = "tcp://proxy"
conf.Proxy.KeyFile = "/etc/teleport/proxy.key"
conf.Proxy.CertFile = "/etc/teleport/proxy.crt"
conf.Proxy.KeyPairs = []KeyPair{
{
PrivateKey: "/etc/teleport/proxy.key",
Certificate: "/etc/teleport/proxy.crt",
},
}
conf.Proxy.ListenAddress = "tcp://proxy_ssh_addr"
conf.Proxy.WebAddr = "tcp://web_addr"
conf.Proxy.TunAddr = "reverse_tunnel_address:3311"
conf.Proxy.IdP.SAMLIdP.EnabledFlag = "true"
conf.Proxy.IdP.SAMLIdP.BaseURL = "https://test-url.com"
// kubernetes service:
conf.Kube = Kube{
Service: Service{
EnabledFlag: "yes",
ListenAddress: "tcp://kube",
},
KubeClusterName: "kube-cluster",
PublicAddr: apiutils.Strings([]string{"kube-host:1234"}),
ResourceMatchers: []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"*": {"*"}},
},
},
}
// Application service.
conf.Apps.EnabledFlag = "yes"
conf.Apps.Apps = []*App{
{
Name: "foo",
URI: "http://127.0.0.1:8080",
PublicAddr: "foo.example.com",
StaticLabels: Labels,
DynamicLabels: CommandLabels,
},
{
Name: "mcp-everything",
StaticLabels: Labels,
MCP: &MCP{
Command: "docker",
Args: []string{"run", "-i", "--rm", "mcp/everything"},
RunAsHostUser: "docker",
},
},
}
conf.Apps.ResourceMatchers = []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"*": {"*"}},
},
}
// Database service.
conf.Databases.EnabledFlag = "yes"
conf.Databases.Databases = []*Database{
{
Name: "postgres",
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
StaticLabels: Labels,
DynamicLabels: CommandLabels,
},
}
conf.Databases.ResourceMatchers = []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"a": {"b"}},
},
{
Labels: map[string]apiutils.Strings{"c": {"d"}},
AWS: ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBAccess",
ExternalID: "externalID123",
},
},
}
conf.Databases.AWSMatchers = []AWSMatcher{
{
Types: []string{"rds"},
Regions: []string{"us-west-1", "us-east-1"},
Tags: map[string]apiutils.Strings{"a": {"b"}},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
{
Types: []string{"rds"},
Regions: []string{"us-central-1"},
Tags: map[string]apiutils.Strings{"c": {"d"}},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
},
}
conf.Databases.AzureMatchers = []AzureMatcher{
{
Subscriptions: []string{"sub1", "sub2"},
ResourceGroups: []string{"rg1", "rg2"},
Types: []string{"mysql"},
Regions: []string{"eastus", "westus"},
ResourceTags: map[string]apiutils.Strings{
"a": {"b"},
},
},
{
Subscriptions: []string{"sub3", "sub4"},
ResourceGroups: []string{"rg3", "rg4"},
Types: []string{"postgres"},
Regions: []string{"centralus"},
ResourceTags: map[string]apiutils.Strings{
"c": {"d"},
},
},
{
Types: []string{"mysql", "postgres"},
Regions: []string{"centralus"},
ResourceTags: map[string]apiutils.Strings{
"e": {"f"},
},
},
}
// Metrics service.
conf.Metrics.EnabledFlag = "yes"
conf.Metrics.ListenAddress = "tcp://metrics"
conf.Metrics.GRPCServerLatency = true
conf.Metrics.GRPCClientLatency = true
conf.Metrics.CACerts = []string{"/etc/teleport/ca.crt"}
conf.Metrics.KeyPairs = []KeyPair{
{
PrivateKey: "/etc/teleport/proxy.key",
Certificate: "/etc/teleport/proxy.crt",
},
}
// Debug service.
conf.Debug.EnabledFlag = "yes"
// Windows Desktop Service
conf.WindowsDesktop = WindowsDesktopService{
Service: Service{
EnabledFlag: "yes",
ListenAddress: "tcp://windows_desktop",
},
PublicAddr: apiutils.Strings([]string{"winsrv.example.com:3028", "no-port.winsrv.example.com"}),
ADHosts: apiutils.Strings([]string{"win.example.com:3389", "no-port.win.example.com"}),
}
// Tracing service.
conf.Tracing.EnabledFlag = "yes"
conf.Tracing.ExporterURL = "https://localhost:4318"
conf.Tracing.SamplingRatePerMillion = 10
conf.Tracing.CACerts = []string{"/etc/teleport/exporter.crt"}
conf.Tracing.KeyPairs = []KeyPair{
{
PrivateKey: "/etc/teleport/exporter.key",
Certificate: "/etc/teleport/exporter.crt",
},
}
return conf.DebugDumpToYAML()
}
func TestPermitUserEnvironment(t *testing.T) {
tests := []struct {
inConfigString string
inPermitUserEnvironment bool
outPermitUserEnvironment bool
}{
// 0 - set on the command line, expect PermitUserEnvironment to be true
{
``,
true,
true,
},
// 1 - set in config file, expect PermitUserEnvironment to be true
{
`
ssh_service:
permit_user_env: true
`,
false,
true,
},
// 2 - not set anywhere, expect PermitUserEnvironment to be false
{
``,
false,
false,
},
}
// run tests
for i, tt := range tests {
comment := fmt.Sprintf("Test %v", i)
clf := CommandLineFlags{
ConfigString: base64.StdEncoding.EncodeToString([]byte(tt.inConfigString)),
PermitUserEnvironment: tt.inPermitUserEnvironment,
}
cfg := servicecfg.MakeDefaultConfig()
err := Configure(&clf, cfg, false)
require.NoError(t, err, comment)
require.Equal(t, tt.outPermitUserEnvironment, cfg.SSH.PermitUserEnvironment, comment)
}
}
func TestSetDefaultListenerAddresses(t *testing.T) {
tests := []struct {
desc string
fc FileConfig
want servicecfg.ProxyConfig
}{
{
desc: "v1 config should set default proxy listen addresses",
fc: FileConfig{
Version: defaults.TeleportConfigVersionV1,
Proxy: Proxy{
Service: Service{
defaultEnabled: true,
},
},
},
want: servicecfg.ProxyConfig{
WebAddr: *utils.MustParseAddr("0.0.0.0:3080"),
ReverseTunnelListenAddr: *utils.MustParseAddr("0.0.0.0:3024"),
SSHAddr: *utils.MustParseAddr("0.0.0.0:3023"),
Enabled: true,
Kube: servicecfg.KubeProxyConfig{
Enabled: false,
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
},
IdP: servicecfg.IdP{
SAMLIdP: servicecfg.SAMLIdP{
Enabled: true,
},
},
},
},
{
desc: "v2 config should not set any default listen addresses",
fc: FileConfig{
Version: defaults.TeleportConfigVersionV2,
Proxy: Proxy{
Service: Service{
defaultEnabled: true,
},
WebAddr: "0.0.0.0:9999",
},
},
want: servicecfg.ProxyConfig{
WebAddr: *utils.MustParseAddr("0.0.0.0:9999"),
Enabled: true,
Kube: servicecfg.KubeProxyConfig{
Enabled: true,
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
},
IdP: servicecfg.IdP{
SAMLIdP: servicecfg.SAMLIdP{
Enabled: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
require.NoError(t, ApplyFileConfig(&tt.fc, cfg))
require.NoError(t, Configure(&CommandLineFlags{}, cfg, false))
opts := cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(servicecfg.ProxyConfig{}, "AutomaticUpgradesChannels"),
}
require.Empty(t, cmp.Diff(cfg.Proxy, tt.want, opts...))
})
}
}
// TestDebugFlag ensures that the debug command-line flag is correctly set in the config.
func TestDebugFlag(t *testing.T) {
clf := CommandLineFlags{
Debug: true,
}
cfg := servicecfg.MakeDefaultConfig()
require.False(t, cfg.Debug)
err := Configure(&clf, cfg, false)
require.NoError(t, err)
require.True(t, cfg.Debug)
}
func TestMergingCAPinConfig(t *testing.T) {
tests := []struct {
desc string
cliPins []string
configPins string // this goes into the yaml in bracket syntax [val1,val2,...]
want []string
}{
{
desc: "pin taken from cli only",
cliPins: []string{"cli-pin"},
configPins: "",
want: []string{"cli-pin"},
},
{
desc: "pin taken from file config only",
cliPins: []string{},
configPins: "fc-pin",
want: []string{"fc-pin"},
},
{
desc: "non-empty pins from cli override file config",
cliPins: []string{"cli-pin1", "", "cli-pin2", ""},
configPins: "fc-pin",
want: []string{"cli-pin1", "cli-pin2"},
},
{
desc: "all empty pins from cli do not override file config",
cliPins: []string{"", ""},
configPins: "fc-pin1,fc-pin2",
want: []string{"fc-pin1", "fc-pin2"},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
clf := CommandLineFlags{
CAPins: tt.cliPins,
ConfigString: base64.StdEncoding.EncodeToString(fmt.Appendf(nil,
configWithCAPins,
tt.configPins,
)),
}
cfg := servicecfg.MakeDefaultConfig()
require.Empty(t, cfg.CAPins)
err := Configure(&clf, cfg, false)
require.NoError(t, err)
require.ElementsMatch(t, tt.want, cfg.CAPins)
})
}
}
func TestLicenseFile(t *testing.T) {
testCases := []struct {
path string
datadir string
result string
}{
// 0 - no license, no data dir
{
path: "",
datadir: "",
result: filepath.Join(defaults.DataDir, defaults.LicenseFile),
},
// 1 - relative path, default data dir
{
path: "lic.pem",
datadir: "",
result: filepath.Join(defaults.DataDir, "lic.pem"),
},
// 2 - relative path, custom data dir
{
path: "baz.pem",
datadir: filepath.Join("foo", "bar"),
result: filepath.Join("foo", "bar", "baz.pem"),
},
// 3 - absolute path
{
path: "/etc/teleport/license",
result: "/etc/teleport/license",
},
}
cfg := servicecfg.MakeDefaultConfig()
for i, tc := range testCases {
t.Run(fmt.Sprintf("test%d", i), func(t *testing.T) {
fc := new(FileConfig)
require.NoError(t, fc.CheckAndSetDefaults())
fc.Auth.LicenseFile = tc.path
fc.DataDir = tc.datadir
err := ApplyFileConfig(fc, cfg)
require.NoError(t, err)
require.Equal(t, tc.result, cfg.Auth.LicenseFile)
})
}
}
func TestLicenseFileNoConfig(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
require.NoError(t, Configure(new(CommandLineFlags), cfg, false /* legacy app flags */))
require.Equal(t, filepath.Join(defaults.DataDir, defaults.LicenseFile), cfg.Auth.LicenseFile)
}
// TestFIPS makes sure configuration is correctly updated/enforced when in
// FedRAMP/FIPS 140-2 mode.
func TestFIPS(t *testing.T) {
tests := []struct {
inConfigString string
inFIPSMode bool
outError bool
}{
{
inConfigString: configWithoutFIPSKex,
inFIPSMode: true,
outError: true,
},
{
inConfigString: configWithoutFIPSKex,
inFIPSMode: false,
outError: false,
},
{
inConfigString: configWithFIPSKex,
inFIPSMode: true,
outError: false,
},
{
inConfigString: configWithFIPSKex,
inFIPSMode: false,
outError: false,
},
}
for i, tt := range tests {
comment := fmt.Sprintf("Test %v", i)
clf := CommandLineFlags{
ConfigString: base64.StdEncoding.EncodeToString([]byte(tt.inConfigString)),
FIPS: tt.inFIPSMode,
}
cfg := servicecfg.MakeDefaultConfig()
servicecfg.ApplyDefaults(cfg)
servicecfg.ApplyFIPSDefaults(cfg)
err := Configure(&clf, cfg, false)
if tt.outError {
require.Error(t, err, comment)
} else {
require.NoError(t, err, comment)
}
}
}
func TestProxyKube(t *testing.T) {
tests := []struct {
desc string
cfg Proxy
version string
want servicecfg.KubeProxyConfig
checkErr require.ErrorAssertionFunc
}{
{
desc: "not configured",
cfg: Proxy{},
want: servicecfg.KubeProxyConfig{
Enabled: false,
},
checkErr: require.NoError,
},
{
desc: "legacy format, no local cluster",
cfg: Proxy{Kube: KubeProxy{
Service: Service{EnabledFlag: "yes", ListenAddress: "0.0.0.0:8080"},
}},
want: servicecfg.KubeProxyConfig{
Enabled: true,
ListenAddr: *utils.MustParseAddr("0.0.0.0:8080"),
LegacyKubeProxy: true,
},
checkErr: require.NoError,
},
{
desc: "legacy format, with local cluster",
cfg: Proxy{Kube: KubeProxy{
Service: Service{EnabledFlag: "yes", ListenAddress: "0.0.0.0:8080"},
KubeconfigFile: "/tmp/kubeconfig",
PublicAddr: apiutils.Strings([]string{constants.KubeTeleportProxyALPNPrefix + "example.com:443"}),
}},
want: servicecfg.KubeProxyConfig{
Enabled: true,
ListenAddr: *utils.MustParseAddr("0.0.0.0:8080"),
KubeconfigPath: "/tmp/kubeconfig",
PublicAddrs: []utils.NetAddr{*utils.MustParseAddr(constants.KubeTeleportProxyALPNPrefix + "example.com:443")},
LegacyKubeProxy: true,
},
checkErr: require.NoError,
},
{
desc: "new format",
cfg: Proxy{KubeAddr: "0.0.0.0:8080"},
want: servicecfg.KubeProxyConfig{
Enabled: true,
ListenAddr: *utils.MustParseAddr("0.0.0.0:8080"),
},
checkErr: require.NoError,
},
{
desc: "new and old formats",
cfg: Proxy{
KubeAddr: "0.0.0.0:8080",
Kube: KubeProxy{
Service: Service{EnabledFlag: "yes", ListenAddress: "0.0.0.0:8080"},
},
},
checkErr: require.Error,
},
{
desc: "new format and old explicitly disabled",
cfg: Proxy{
KubeAddr: "0.0.0.0:8080",
Kube: KubeProxy{
Service: Service{EnabledFlag: "no", ListenAddress: "0.0.0.0:8080"},
KubeconfigFile: "/tmp/kubeconfig",
PublicAddr: apiutils.Strings([]string{constants.KubeTeleportProxyALPNPrefix + "example.com:443"}),
},
},
want: servicecfg.KubeProxyConfig{
Enabled: true,
ListenAddr: *utils.MustParseAddr("0.0.0.0:8080"),
},
checkErr: require.NoError,
},
{
desc: "v2 kube service should be enabled by default",
version: defaults.TeleportConfigVersionV2,
cfg: Proxy{},
want: servicecfg.KubeProxyConfig{
Enabled: true,
},
checkErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
fc := &FileConfig{
Version: tt.version,
Proxy: tt.cfg,
}
cfg := &servicecfg.Config{
Version: tt.version,
}
err := applyProxyConfig(fc, cfg)
tt.checkErr(t, err)
require.Empty(t, cmp.Diff(cfg.Proxy.Kube, tt.want, cmpopts.EquateEmpty()))
})
}
}
// Test default values generated by v1 and v2 configuration versions.
func TestProxyConfigurationVersion(t *testing.T) {
tests := []struct {
desc string
fc FileConfig
want servicecfg.ProxyConfig
checkErr require.ErrorAssertionFunc
}{
{
desc: "v2 config with default web address",
fc: FileConfig{
Version: defaults.TeleportConfigVersionV2,
Proxy: Proxy{
Service: Service{
defaultEnabled: true,
},
},
},
want: servicecfg.ProxyConfig{
WebAddr: *utils.MustParseAddr("0.0.0.0:3080"),
Enabled: true,
Kube: servicecfg.KubeProxyConfig{
Enabled: true,
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
},
IdP: servicecfg.IdP{
SAMLIdP: servicecfg.SAMLIdP{
Enabled: true,
},
},
},
checkErr: require.NoError,
},
{
desc: "v2 config with custom web address",
fc: FileConfig{
Version: defaults.TeleportConfigVersionV2,
Proxy: Proxy{
Service: Service{
defaultEnabled: true,
},
WebAddr: "0.0.0.0:9999",
},
},
want: servicecfg.ProxyConfig{
Enabled: true,
WebAddr: *utils.MustParseAddr("0.0.0.0:9999"),
Kube: servicecfg.KubeProxyConfig{
Enabled: true,
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
},
IdP: servicecfg.IdP{
SAMLIdP: servicecfg.SAMLIdP{
Enabled: true,
},
},
},
checkErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err := ApplyFileConfig(&tt.fc, cfg)
tt.checkErr(t, err)
opts := cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(servicecfg.ProxyConfig{}, "AutomaticUpgradesChannels"),
}
require.Empty(t, cmp.Diff(cfg.Proxy, tt.want, opts...))
})
}
}
func TestWindowsDesktopService(t *testing.T) {
t.Parallel()
for _, test := range []struct {
desc string
mutate func(fc *FileConfig)
expectError require.ErrorAssertionFunc
}{
{
desc: "NOK - invalid static host addr",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.ADHosts = []string{"badscheme://foo:1:2"}
},
},
{
desc: "NOK - invalid host label key",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.HostLabels = []WindowsHostLabelRule{
{Match: ".*", Labels: map[string]string{"invalid label key": "value"}},
}
},
},
{
desc: "NOK - invalid host label regexp",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.HostLabels = []WindowsHostLabelRule{
{Match: "g(-z]+ invalid regex", Labels: map[string]string{"key": "value"}},
}
},
},
{
desc: "NOK - invalid label key for LDAP attribute",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery.LabelAttributes = []string{"this?is not* a valid key 🚨"}
},
},
{
desc: "NOK - hosts specified but ldap not specified",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.ADHosts = []string{"127.0.0.1:3389"}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "",
}
},
},
{
desc: "OK - hosts specified and ldap specified",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.ADHosts = []string{"127.0.0.1:3389"}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "something",
}
},
},
{
desc: "OK - no hosts specified and ldap not specified",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.ADHosts = []string{}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "",
}
},
},
{
desc: "OK - no hosts specified and ldap specified",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.ADHosts = []string{}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "something",
}
},
},
{
desc: "NOK - discovery specified but ldap not specified",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery = LDAPDiscoveryConfig{
BaseDN: "something",
}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "",
}
},
},
{
desc: "OK - discovery specified and ldap specified",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery = LDAPDiscoveryConfig{
BaseDN: "something",
}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "something",
}
},
},
{
desc: "OK - discovery not specified and ldap not specified",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery = LDAPDiscoveryConfig{
BaseDN: "",
}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "",
}
},
},
{
desc: "OK - discovery not specified and ldap specified",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery = LDAPDiscoveryConfig{
BaseDN: "",
}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "something",
}
},
},
{
desc: "NOK - legacy discovery and new discovery_configs both specified",
expectError: require.Error,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.Discovery = LDAPDiscoveryConfig{
BaseDN: "*",
}
fc.WindowsDesktop.DiscoveryConfigs = []LDAPDiscoveryConfig{
{BaseDN: "OU=stage,DC=example,DC=com"},
{BaseDN: "OU=prod,DC=example,DC=com"},
}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "something",
}
},
},
{
desc: "OK - valid config",
expectError: require.NoError,
mutate: func(fc *FileConfig) {
fc.WindowsDesktop.EnabledFlag = "yes"
fc.WindowsDesktop.ListenAddress = "0.0.0.0:3028"
fc.WindowsDesktop.ADHosts = []string{"127.0.0.1:3389"}
fc.WindowsDesktop.LDAP = LDAPConfig{
Addr: "something",
}
fc.WindowsDesktop.HostLabels = []WindowsHostLabelRule{
{Match: ".*", Labels: map[string]string{"key": "value"}},
}
},
},
} {
t.Run(test.desc, func(t *testing.T) {
fc := &FileConfig{}
test.mutate(fc)
cfg := &servicecfg.Config{}
err := applyWindowsDesktopConfig(fc, cfg)
test.expectError(t, err)
})
}
}
func TestApps(t *testing.T) {
tests := []struct {
inConfigString string
name string
outErr require.ErrorAssertionFunc
}{
{
inConfigString: `
app_service:
enabled: true
apps:
-
name: foo
public_addr: "foo.example.com"
uri: "http://127.0.0.1:8080"
resources:
- labels:
'*': '*'
`,
name: "app and wildcard resources",
outErr: require.NoError,
},
{
inConfigString: `
app_service:
enabled: true
apps:
-
public_addr: "foo.example.com"
uri: "http://127.0.0.1:8080"
`,
name: "config is missing name",
outErr: require.Error,
},
{
inConfigString: `
app_service:
enabled: true
apps:
-
name: foo
uri: "http://127.0.0.1:8080"
`,
name: "app",
outErr: require.NoError,
},
{
inConfigString: `
app_service:
enabled: true
apps:
-
name: foo
public_addr: "foo.example.com"
`,
name: "config is missing internal address",
outErr: require.Error,
},
{
inConfigString: `
app_service:
enabled: true
apps:
-
name: foo
public_addr: "foo.example.com"
uri: "http://127.0.0.1:8080"
resources:
- labels:
'*': '*'
aws:
assume_role_arn: "arn:aws:iam::123456789012:role/AppAccess"
`,
name: "assume_role_arn is not supported",
outErr: require.Error,
},
{
inConfigString: `
app_service:
enabled: true
apps:
- name: foo
uri: "tcp://127.0.0.1"
tcp_ports:
- port: 1234
- port: 30000
end_port: 30768
`,
name: "TCP app with ports",
outErr: require.NoError,
},
{
inConfigString: `
app_service:
enabled: true
apps:
- name: foo
uri: "tcp://127.0.0.1"
tcp_ports:
- end_port: 30000
`,
name: "TCP app with only end port",
outErr: require.Error,
},
{
inConfigString: `
app_service:
enabled: true
apps:
-
name: foo
use_any_proxy_public_addr: true
uri: "http://127.0.0.1:8080"
`,
name: "app with use_any_proxy_public_addr",
outErr: require.NoError,
},
{
inConfigString: `
app_service:
enabled: true
apps:
- name: foo
uri: "tcp://127.0.0.1"
tcp_ports:
- port: 78787
`,
name: "TCP app with port bigger than 65535",
outErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clf := CommandLineFlags{
ConfigString: base64.StdEncoding.EncodeToString([]byte(tt.inConfigString)),
}
cfg := servicecfg.MakeDefaultConfig()
err := Configure(&clf, cfg, false)
tt.outErr(t, err)
})
}
}
// TestAppsCLF checks that validation runs on application configuration passed
// in on the command line.
func TestAppsCLF(t *testing.T) {
tests := []struct {
desc string
inRoles string
inAppName string
inAppURI string
inAppCloud string
inLegacyAppFlags bool
inMCPDemoServer bool
outApps []servicecfg.App
outMCPDemoServer bool
requireError require.ErrorAssertionFunc
}{
{
desc: "role provided, valid name and uri",
inRoles: defaults.RoleApp,
inAppName: "foo",
inAppURI: "http://localhost:8080",
outApps: []servicecfg.App{
{
Name: "foo",
URI: "http://localhost:8080",
StaticLabels: map[string]string{"teleport.dev/origin": "config-file"},
DynamicLabels: map[string]types.CommandLabel{},
},
},
requireError: require.NoError,
},
{
desc: "role provided, name not provided, uri not provided, legacy flags",
inRoles: defaults.RoleApp,
inAppName: "",
inAppURI: "",
inLegacyAppFlags: true,
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "application name (--app-name) and URI (--app-uri) flags are both required to join application proxy to the cluster")
},
},
{
desc: "role provided, name not provided, uri not provided, regular flags",
inRoles: defaults.RoleApp,
inAppName: "",
inAppURI: "",
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "to join application proxy to the cluster provide application name (--name) and either URI (--uri) or Cloud type (--cloud)")
},
},
{
desc: "role provided, name not provided, legacy flags",
inRoles: defaults.RoleApp,
inAppName: "",
inAppURI: "http://localhost:8080",
inLegacyAppFlags: true,
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "application name (--app-name) is required to join application proxy to the cluster")
},
},
{
desc: "role provided, name not provided, regular flags",
inRoles: defaults.RoleApp,
inAppName: "",
inAppURI: "http://localhost:8080",
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "to join application proxy to the cluster provide application name (--name)")
},
},
{
desc: "role provided, uri not provided, legacy flags",
inRoles: defaults.RoleApp,
inAppName: "foo",
inAppURI: "",
inLegacyAppFlags: true,
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "URI (--app-uri) flag is required to join application proxy to the cluster")
},
},
{
desc: "role provided, uri not provided, regular flags",
inRoles: defaults.RoleApp,
inAppName: "foo",
inAppURI: "",
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "to join application proxy to the cluster provide URI (--uri) or Cloud type (--cloud)")
},
},
{
desc: "valid name and uri",
inAppName: "foo",
inAppURI: "http://localhost:8080",
outApps: []servicecfg.App{
{
Name: "foo",
URI: "http://localhost:8080",
StaticLabels: map[string]string{"teleport.dev/origin": "config-file"},
DynamicLabels: map[string]types.CommandLabel{},
},
},
requireError: require.NoError,
},
{
desc: "valid name and cloud",
inAppName: "foo",
inAppCloud: types.CloudGCP,
outApps: []servicecfg.App{
{
Name: "foo",
URI: "cloud://GCP",
Cloud: "GCP",
StaticLabels: map[string]string{"teleport.dev/origin": "config-file"},
DynamicLabels: map[string]types.CommandLabel{},
},
},
requireError: require.NoError,
},
{
desc: "invalid name",
inAppName: "-foo",
inAppURI: "http://localhost:8080",
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "application name \"-foo\" must be a lower case valid DNS subdomain: https://goteleport.com/docs/enroll-resources/application-access/guides/connecting-apps/#application-name")
},
},
{
desc: "missing uri",
inAppName: "foo",
outApps: nil,
requireError: func(t require.TestingT, err error, i ...any) {
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "missing application \"foo\" URI")
},
},
{
desc: "mcp demo server",
inRoles: defaults.RoleApp,
inAppName: "",
inAppURI: "",
inMCPDemoServer: true,
outApps: nil,
outMCPDemoServer: true,
requireError: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
clf := CommandLineFlags{
Roles: tt.inRoles,
AppName: tt.inAppName,
AppURI: tt.inAppURI,
AppCloud: tt.inAppCloud,
MCPDemoServer: tt.inMCPDemoServer,
}
cfg := servicecfg.MakeDefaultConfig()
err := Configure(&clf, cfg, tt.inLegacyAppFlags)
tt.requireError(t, err)
require.Equal(t, tt.outApps, cfg.Apps.Apps)
require.Equal(t, tt.outMCPDemoServer, cfg.Apps.MCPDemoServer)
})
}
}
// TestDatabaseConfig ensures reading database the configuration won't return
// error.
func TestDatabaseConfig(t *testing.T) {
tests := []struct {
inConfigString string
desc string
}{
{
desc: "valid database config",
inConfigString: `
db_service:
enabled: true
resources:
- labels:
'*': '*'
aws:
- types: ["rds", "redshift"]
regions: ["us-east-1", "us-west-1"]
tags:
'*': '*'
azure:
- subscriptions: ["foo", "bar"]
types: ["mysql", "postgres"]
regions: ["eastus", "westus"]
tags:
'*': '*'
databases:
- name: foo
protocol: postgres
uri: localhost:5432
static_labels:
env: test
dynamic_labels:
- name: arch
command: ["uname", "-p"]
period: 1h
`,
},
{
desc: "missing database name",
inConfigString: `
db_service:
enabled: true
databases:
- protocol: postgres
uri: localhost:5432
`,
},
{
desc: "unsupported database protocol",
inConfigString: `
db_service:
enabled: true
databases:
- name: foo
protocol: unknown
uri: localhost:5432
`,
},
{
desc: "missing database uri",
inConfigString: `
db_service:
enabled: true
databases:
- name: foo
protocol: postgres
`,
},
{
desc: "invalid database uri (missing port)",
inConfigString: `
db_service:
enabled: true
databases:
- name: foo
protocol: postgres
uri: 192.168.1.1
`,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
clf := CommandLineFlags{
ConfigString: base64.StdEncoding.EncodeToString([]byte(tt.inConfigString)),
}
err := Configure(&clf, servicecfg.MakeDefaultConfig(), false)
require.NoError(t, err)
})
}
}
// TestDatabaseCLIFlags verifies database service can be configured with CLI flags.
func TestDatabaseCLIFlags(t *testing.T) {
// Prepare test CA certificate used to configure some databases.
testCertPath := filepath.Join(t.TempDir(), "cert.pem")
err := os.WriteFile(testCertPath, fixtures.LocalhostCert, 0o644)
require.NoError(t, err)
tests := []struct {
inFlags CommandLineFlags
desc string
outDatabase servicecfg.Database
}{
{
desc: "valid database config",
inFlags: CommandLineFlags{
DatabaseName: "foo",
DatabaseProtocol: defaults.ProtocolPostgres,
DatabaseURI: "localhost:5432",
Labels: "env=test,hostname=[1h:hostname]",
},
outDatabase: servicecfg.Database{
Name: "foo",
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
StaticLabels: map[string]string{
"env": "test",
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{
"hostname": &types.CommandLabelV2{
Period: types.Duration(time.Hour),
Command: []string{"hostname"},
},
},
},
},
{
desc: "RDS database",
inFlags: CommandLineFlags{
DatabaseName: "rds",
DatabaseProtocol: defaults.ProtocolMySQL,
DatabaseURI: "localhost:3306",
DatabaseAWSRegion: "us-east-1",
DatabaseAWSAccountID: "123456789012",
DatabaseAWSAssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
DatabaseAWSExternalID: "externalID123",
},
outDatabase: servicecfg.Database{
Name: "rds",
Protocol: defaults.ProtocolMySQL,
URI: "localhost:3306",
AWS: servicecfg.DatabaseAWS{
Region: "us-east-1",
AccountID: "123456789012", // this gets derived from the assumed role.
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "Redshift database",
inFlags: CommandLineFlags{
DatabaseName: "redshift",
DatabaseProtocol: defaults.ProtocolPostgres,
DatabaseURI: "localhost:5432",
DatabaseAWSRegion: "us-east-1",
DatabaseAWSRedshiftClusterID: "redshift-cluster-1",
DatabaseAWSAssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
DatabaseAWSExternalID: "externalID123",
},
outDatabase: servicecfg.Database{
Name: "redshift",
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: servicecfg.DatabaseAWS{
Region: "us-east-1",
Redshift: servicecfg.DatabaseAWSRedshift{
ClusterID: "redshift-cluster-1",
},
AccountID: "123456789012", // this gets derived from the assumed role.
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "Cloud SQL database",
inFlags: CommandLineFlags{
DatabaseName: "gcp",
DatabaseProtocol: defaults.ProtocolPostgres,
DatabaseURI: "localhost:5432",
DatabaseCACertFile: testCertPath,
DatabaseGCPProjectID: "gcp-project-1",
DatabaseGCPInstanceID: "gcp-instance-1",
},
outDatabase: servicecfg.Database{
Name: "gcp",
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
TLS: servicecfg.DatabaseTLS{
CACert: fixtures.LocalhostCert,
},
GCP: servicecfg.DatabaseGCP{
ProjectID: "gcp-project-1",
InstanceID: "gcp-instance-1",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "SQL Server",
inFlags: CommandLineFlags{
DatabaseName: "sqlserver",
DatabaseProtocol: defaults.ProtocolSQLServer,
DatabaseURI: "sqlserver.example.com:1433",
DatabaseADKeytabFile: "/etc/keytab",
DatabaseADDomain: "EXAMPLE.COM",
DatabaseADSPN: "MSSQLSvc/sqlserver.example.com:1433",
},
outDatabase: servicecfg.Database{
Name: "sqlserver",
Protocol: defaults.ProtocolSQLServer,
URI: "sqlserver.example.com:1433",
AD: servicecfg.DatabaseAD{
KeytabFile: "/etc/keytab",
Domain: "EXAMPLE.COM",
SPN: "MSSQLSvc/sqlserver.example.com:1433",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "MySQL version",
inFlags: CommandLineFlags{
DatabaseName: "mysql-foo",
DatabaseProtocol: defaults.ProtocolMySQL,
DatabaseURI: "localhost:3306",
DatabaseMySQLServerVersion: "8.0.28",
},
outDatabase: servicecfg.Database{
Name: "mysql-foo",
Protocol: defaults.ProtocolMySQL,
URI: "localhost:3306",
MySQL: servicecfg.MySQLOptions{
ServerVersion: "8.0.28",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "AWS Keyspaces",
inFlags: CommandLineFlags{
DatabaseName: "keyspace",
DatabaseProtocol: defaults.ProtocolCassandra,
DatabaseURI: "cassandra.us-east-1.amazonaws.com:9142",
DatabaseAWSAccountID: "123456789012",
DatabaseAWSRegion: "us-east-1",
DatabaseAWSAssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
DatabaseAWSExternalID: "externalID123",
},
outDatabase: servicecfg.Database{
Name: "keyspace",
Protocol: defaults.ProtocolCassandra,
URI: "cassandra.us-east-1.amazonaws.com:9142",
AWS: servicecfg.DatabaseAWS{
Region: "us-east-1",
AccountID: "123456789012",
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "AWS DynamoDB",
inFlags: CommandLineFlags{
DatabaseName: "ddb",
DatabaseProtocol: defaults.ProtocolDynamoDB,
DatabaseURI: "dynamodb.us-east-1.amazonaws.com",
DatabaseAWSAccountID: "123456789012",
DatabaseAWSRegion: "us-east-1",
DatabaseAWSAssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
DatabaseAWSExternalID: "externalID123",
},
outDatabase: servicecfg.Database{
Name: "ddb",
Protocol: defaults.ProtocolDynamoDB,
URI: "dynamodb.us-east-1.amazonaws.com",
AWS: servicecfg.DatabaseAWS{
Region: "us-east-1",
AccountID: "123456789012",
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
{
desc: "AWS DynamoDB with session tags",
inFlags: CommandLineFlags{
DatabaseName: "ddb",
DatabaseProtocol: defaults.ProtocolDynamoDB,
DatabaseURI: "dynamodb.us-east-1.amazonaws.com",
DatabaseAWSAccountID: "123456789012",
DatabaseAWSRegion: "us-east-1",
DatabaseAWSAssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
DatabaseAWSExternalID: "externalID123",
DatabaseAWSSessionTags: "database_name=hello,something=else",
},
outDatabase: servicecfg.Database{
Name: "ddb",
Protocol: defaults.ProtocolDynamoDB,
URI: "dynamodb.us-east-1.amazonaws.com",
AWS: servicecfg.DatabaseAWS{
Region: "us-east-1",
AccountID: "123456789012",
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
SessionTags: map[string]string{
"database_name": "hello",
"something": "else",
},
},
StaticLabels: map[string]string{
types.OriginLabel: types.OriginConfigFile,
},
DynamicLabels: services.CommandLabels{},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
config := servicecfg.MakeDefaultConfig()
err := Configure(&tt.inFlags, config, false)
require.NoError(t, err)
require.Equal(t, []servicecfg.Database{tt.outDatabase}, config.Databases.Databases)
})
}
}
func TestTLSCert(t *testing.T) {
tmpDir := t.TempDir()
tmpCA := filepath.Join(tmpDir, "ca.pem")
err := os.WriteFile(tmpCA, fixtures.LocalhostCert, 0o644)
require.NoError(t, err)
tests := []struct {
name string
conf *FileConfig
}{
{
"read deprecated DB cert field",
&FileConfig{
Databases: Databases{
Service: Service{
EnabledFlag: "true",
},
Databases: []*Database{
{
Name: "test-db-1",
Protocol: "mysql",
URI: "localhost:1234",
CACertFile: tmpCA,
},
},
},
},
},
{
"read DB cert field",
&FileConfig{
Databases: Databases{
Service: Service{
EnabledFlag: "true",
},
Databases: []*Database{
{
Name: "test-db-1",
Protocol: "mysql",
URI: "localhost:1234",
TLS: DatabaseTLS{
CACertFile: tmpCA,
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(tt.conf, cfg)
require.NoError(t, err)
require.Len(t, cfg.Databases.Databases, 1)
require.Equal(t, fixtures.LocalhostCert, cfg.Databases.Databases[0].TLS.CACert)
})
}
}
func TestApplyKeyStoreConfig(t *testing.T) {
slotNumber := 1
tempDir := t.TempDir()
worldReadablePinFilePath := filepath.Join(tempDir, "world-readable-pin-file")
err := os.WriteFile(worldReadablePinFilePath, []byte("world-readable-pin-file"), 0o644)
require.NoError(t, err)
securePinFilePath := filepath.Join(tempDir, "secure-pin-file")
err = os.WriteFile(securePinFilePath, []byte("secure-pin-file"), 0o600)
require.NoError(t, err)
worldWritablePKCS11LibPath := filepath.Join(tempDir, "world-writable-pkcs1")
err = os.WriteFile(worldWritablePKCS11LibPath, []byte("pkcs11"), 0o666)
require.NoError(t, err)
require.NoError(t, os.Chmod(worldWritablePKCS11LibPath, 0o666))
securePKCS11LibPath := filepath.Join(tempDir, "secure-pkcs11")
err = os.WriteFile(securePKCS11LibPath, []byte("pkcs11"), 0o600)
require.NoError(t, err)
tests := []struct {
name string
auth Auth
want servicecfg.KeystoreConfig
errMessage string
}{
{
name: "handle nil configuration",
auth: Auth{
CAKeyParams: nil,
},
want: servicecfg.MakeDefaultConfig().Auth.KeyStore,
},
{
name: "correct config",
auth: Auth{
CAKeyParams: &CAKeyParams{
PKCS11: &PKCS11{
ModulePath: securePKCS11LibPath,
TokenLabel: "foo",
SlotNumber: &slotNumber,
PIN: "pin",
},
},
},
want: servicecfg.KeystoreConfig{
PKCS11: servicecfg.PKCS11Config{
TokenLabel: "foo",
SlotNumber: &slotNumber,
PIN: "pin",
Path: securePKCS11LibPath,
},
},
},
{
name: "correct config with pin file",
auth: Auth{
CAKeyParams: &CAKeyParams{
PKCS11: &PKCS11{
ModulePath: securePKCS11LibPath,
TokenLabel: "foo",
SlotNumber: &slotNumber,
PINPath: securePinFilePath,
},
},
},
want: servicecfg.KeystoreConfig{
PKCS11: servicecfg.PKCS11Config{
TokenLabel: "foo",
SlotNumber: &slotNumber,
PIN: "secure-pin-file",
Path: securePKCS11LibPath,
},
},
},
{
name: "err when pin and pin path configured",
auth: Auth{
CAKeyParams: &CAKeyParams{
PKCS11: &PKCS11{
PIN: "oops",
PINPath: securePinFilePath,
},
},
},
errMessage: "can not set both pin and pin_path",
},
{
name: "err when pkcs11 world writable",
auth: Auth{
CAKeyParams: &CAKeyParams{
PKCS11: &PKCS11{
ModulePath: worldWritablePKCS11LibPath,
},
},
},
errMessage: fmt.Sprintf(
"PKCS11 library (%s) must not be world-writable",
worldWritablePKCS11LibPath,
),
},
{
name: "err when pin file world-readable",
auth: Auth{
CAKeyParams: &CAKeyParams{
PKCS11: &PKCS11{
PINPath: worldReadablePinFilePath,
},
},
},
errMessage: fmt.Sprintf(
"HSM pin file (%s) must not be world-readable",
worldReadablePinFilePath,
),
},
{
name: "correct config with max sessions",
auth: Auth{
CAKeyParams: &CAKeyParams{
PKCS11: &PKCS11{
ModulePath: securePKCS11LibPath,
TokenLabel: "foo",
SlotNumber: &slotNumber,
MaxSessions: 100,
},
},
},
want: servicecfg.KeystoreConfig{
PKCS11: servicecfg.PKCS11Config{
TokenLabel: "foo",
SlotNumber: &slotNumber,
MaxSessions: 100,
Path: securePKCS11LibPath,
},
},
},
{
name: "correct gcp config",
auth: Auth{
CAKeyParams: &CAKeyParams{
GoogleCloudKMS: &GoogleCloudKMS{
KeyRing: "/projects/my-project/locations/global/keyRings/my-keyring",
ProtectionLevel: "HSM",
},
},
},
want: servicecfg.KeystoreConfig{
GCPKMS: servicecfg.GCPKMSConfig{
KeyRing: "/projects/my-project/locations/global/keyRings/my-keyring",
ProtectionLevel: "HSM",
},
},
},
{
name: "gcp config no protection level",
auth: Auth{
CAKeyParams: &CAKeyParams{
GoogleCloudKMS: &GoogleCloudKMS{
KeyRing: "/projects/my-project/locations/global/keyRings/my-keyring",
},
},
},
errMessage: "must set protection_level in ca_key_params.gcp_kms",
},
{
name: "gcp config no keyring",
auth: Auth{
CAKeyParams: &CAKeyParams{
GoogleCloudKMS: &GoogleCloudKMS{
ProtectionLevel: "HSM",
},
},
},
errMessage: "must set keyring in ca_key_params.gcp_kms",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := servicecfg.MakeDefaultConfig()
err := applyKeyStoreConfig(&FileConfig{
Auth: tt.auth,
}, cfg)
if tt.errMessage != "" {
require.EqualError(t, err, tt.errMessage)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, cfg.Auth.KeyStore)
}
})
}
}
// TestApplyConfigSessionRecording checks if the session recording origin is
// set correct and if file configuration is read and applied correctly.
func TestApplyConfigSessionRecording(t *testing.T) {
tests := []struct {
desc string
inSessionRecording string
inProxyChecksHostKeys string
outOrigin string
outSessionRecording string
outProxyChecksHostKeys bool
}{
{
desc: "both-empty",
inSessionRecording: "",
inProxyChecksHostKeys: "",
outOrigin: "defaults",
outSessionRecording: "node",
outProxyChecksHostKeys: true,
},
{
desc: "proxy-checks-empty",
inSessionRecording: "session_recording: proxy-sync",
inProxyChecksHostKeys: "",
outOrigin: "config-file",
outSessionRecording: "proxy-sync",
outProxyChecksHostKeys: true,
},
{
desc: "session-recording-empty",
inSessionRecording: "",
inProxyChecksHostKeys: "proxy_checks_host_keys: true",
outOrigin: "config-file",
outSessionRecording: "node",
outProxyChecksHostKeys: true,
},
{
desc: "both-set",
inSessionRecording: "session_recording: node-sync",
inProxyChecksHostKeys: "proxy_checks_host_keys: false",
outOrigin: "config-file",
outSessionRecording: "node-sync",
outProxyChecksHostKeys: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
fileconfig := fmt.Sprintf(configSessionRecording,
tt.inSessionRecording,
tt.inProxyChecksHostKeys)
conf, err := ReadConfig(bytes.NewBufferString(fileconfig))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
require.NoError(t, err)
require.Equal(t, tt.outOrigin, cfg.Auth.SessionRecordingConfig.Origin())
require.Equal(t, tt.outSessionRecording, cfg.Auth.SessionRecordingConfig.GetMode())
require.Equal(t, tt.outProxyChecksHostKeys, cfg.Auth.SessionRecordingConfig.GetProxyChecksHostKeys())
})
}
}
func TestJoinParams(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
desc string
input string
expectToken string
expectJoinMethod types.JoinMethod
expectError bool
}{
{
desc: "empty",
},
{
desc: "auth_token",
input: `
teleport:
auth_token: xxxyyy
`,
expectToken: "xxxyyy",
expectJoinMethod: types.JoinMethodToken,
},
{
desc: "join_params token",
input: `
teleport:
join_params:
token_name: xxxyyy
method: token
`,
expectToken: "xxxyyy",
expectJoinMethod: types.JoinMethodToken,
},
{
desc: "join_params ec2",
input: `
teleport:
join_params:
token_name: xxxyyy
method: ec2
`,
expectToken: "xxxyyy",
expectJoinMethod: types.JoinMethodEC2,
},
{
desc: "join_params iam",
input: `
teleport:
join_params:
token_name: xxxyyy
method: iam
`,
expectToken: "xxxyyy",
expectJoinMethod: types.JoinMethodIAM,
},
{
desc: "join_params invalid",
input: `
teleport:
join_params:
token_name: xxxyyy
method: invalid
`,
expectError: true,
},
{
desc: "both set",
input: `
teleport:
auth_token: xxxyyy
join_params:
token_name: xxxyyy
method: iam
`,
expectError: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
conf, err := ReadConfig(strings.NewReader(tc.input))
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
token, err := cfg.Token()
require.NoError(t, err)
require.Equal(t, tc.expectToken, token)
require.Equal(t, tc.expectJoinMethod, cfg.JoinMethod)
})
}
}
func TestApplyConfig_JamfService(t *testing.T) {
tempDir := t.TempDir()
// Write a password file, valid configs require one.
const password = "supersecret!!1!"
passwordFile := filepath.Join(tempDir, "test_jamf_password.txt")
require.NoError(t,
os.WriteFile(passwordFile, []byte(password+"\n"), 0o400),
"WriteFile(%q) failed", passwordFile)
minimalYAML := fmt.Sprintf(`
jamf_service:
enabled: true
api_endpoint: https://yourtenant.jamfcloud.com
username: llama
password_file: %v
`, passwordFile)
tests := []struct {
name string
yaml string
wantErr string
want servicecfg.JamfConfig
}{
{
name: "minimal config",
yaml: minimalYAML,
want: servicecfg.JamfConfig{
Spec: &types.JamfSpecV1{
Enabled: true,
ApiEndpoint: "https://yourtenant.jamfcloud.com",
},
Credentials: &servicecfg.JamfCredentials{
Username: "llama",
Password: password,
},
},
},
{
name: "using API credentials",
yaml: fmt.Sprintf(`jamf_service:
enabled: true
api_endpoint: https://yourtenant.jamfcloud.com
client_id: llama-UUID
client_secret_file: %v`, passwordFile),
want: servicecfg.JamfConfig{
Spec: &types.JamfSpecV1{
Enabled: true,
ApiEndpoint: "https://yourtenant.jamfcloud.com",
},
Credentials: &servicecfg.JamfCredentials{
ClientID: "llama-UUID",
ClientSecret: password,
},
},
},
{
name: "all fields",
yaml: minimalYAML + ` name: jamf2
sync_delay: 1m
exit_on_sync: true
inventory:
- filter_rsql: 1==1
sync_period_partial: 4h
sync_period_full: 48h
on_missing: NOOP
page_size: 10
- {}`,
want: servicecfg.JamfConfig{
Spec: &types.JamfSpecV1{
Enabled: true,
Name: "jamf2",
SyncDelay: types.Duration(1 * time.Minute),
ApiEndpoint: "https://yourtenant.jamfcloud.com",
Inventory: []*types.JamfInventoryEntry{
{
FilterRsql: "1==1",
SyncPeriodPartial: types.Duration(4 * time.Hour),
SyncPeriodFull: types.Duration(48 * time.Hour),
OnMissing: "NOOP",
PageSize: 10,
},
{},
},
},
Credentials: &servicecfg.JamfCredentials{
Username: "llama",
Password: password,
},
ExitOnSync: true,
},
},
{
name: "listen_addr not supported",
yaml: minimalYAML + ` listen_addr: localhost:55555`,
wantErr: "listen_addr",
},
{
name: "password_file invalid",
yaml: `
jamf_service:
enabled: true
api_endpoint: https://yourtenant.jamfcloud.com
username: llama
password_file: /path/to/file/that/doesnt/exist.txt`,
wantErr: "password_file",
},
{
name: "client_secret_file invalid",
yaml: `
jamf_service:
enabled: true
api_endpoint: https://yourtenant.jamfcloud.com
client_id: llama-UUID
client_secret_file: /path/to/file/that/doesnt/exist.txt`,
wantErr: "client_secret_file",
},
{
name: "spec is validated",
yaml: minimalYAML + ` inventory:
- on_missing: BANANA`,
wantErr: "on_missing",
},
{
name: "absent config ignored",
yaml: ``,
},
{
name: "empty config ignored",
yaml: `jamf_service: {}`,
},
{
name: "disabled config ignored",
yaml: `
jamf_service:
enabled: false
api_endpoint: https://yourtenant.jamfcloud.com
username: llama`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fc, err := ReadConfig(strings.NewReader(test.yaml))
require.NoError(t, err, "ReadConfig failed")
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(fc, cfg)
if test.wantErr == "" {
require.NoError(t, err, "ApplyFileConfig failed")
} else {
assert.ErrorContains(t, err, test.wantErr, "ApplyFileConfig error mismatch")
return
}
if diff := cmp.Diff(test.want, cfg.Jamf, protocmp.Transform()); diff != "" {
t.Errorf("ApplyFileConfig: JamfConfig mismatch (-want +got)\n%s", diff)
}
})
}
}
func TestAuthHostedPlugins(t *testing.T) {
t.Parallel()
badParameter := func(t require.TestingT, err error, msgAndArgs ...any) {
require.Error(t, err)
require.True(t, trace.IsBadParameter(err), `expected "bad parameter", but got %v`, err)
}
notExist := func(t require.TestingT, err error, msgAndArgs ...any) {
require.Error(t, err)
require.ErrorIs(t, err, os.ErrNotExist, `expected "does not exist", but got %v`, err)
}
tmpDir := t.TempDir()
clientIDFile := filepath.Join(tmpDir, "id")
clientSecretFile := filepath.Join(tmpDir, "secret")
err := os.WriteFile(clientIDFile, []byte("foo\n"), 0o777)
require.NoError(t, err)
err = os.WriteFile(clientSecretFile, []byte("bar\n"), 0o777)
require.NoError(t, err)
tests := []struct {
desc string
config string
readErr require.ErrorAssertionFunc
applyErr require.ErrorAssertionFunc
assert func(t *testing.T, p servicecfg.HostedPluginsConfig)
}{
{
desc: "Plugins enabled by default",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
}, "\n"),
readErr: require.NoError,
applyErr: require.NoError,
assert: func(t *testing.T, p servicecfg.HostedPluginsConfig) {
require.True(t, p.Enabled)
},
},
{
desc: "Unknown OAuth provider specified",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
" hosted_plugins:",
" enabled: yes",
" oauth_providers:",
" acmecorp:",
" client_id: foo",
" client_secret: bar",
}, "\n"),
readErr: require.Error,
applyErr: require.NoError,
},
{
desc: "OAuth client ID without the secret",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
" hosted_plugins:",
" enabled: yes",
" oauth_providers:",
" slack:",
" client_id: foo",
}, "\n"),
readErr: require.NoError,
applyErr: badParameter,
},
{
desc: "OAuth client secret without the ID",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
" hosted_plugins:",
" enabled: yes",
" oauth_providers:",
" slack:",
" client_secret: bar",
}, "\n"),
readErr: require.NoError,
applyErr: badParameter,
},
{
desc: "OAuth provider in non-existent file",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
"",
" hosted_plugins:",
" enabled: yes",
" oauth_providers:",
" slack:",
" client_id: /tmp/this-does-not-exist",
" client_secret: " + clientSecretFile,
}, "\n"),
readErr: require.NoError,
applyErr: notExist,
},
{
desc: "OAuth provider in existent files",
config: strings.Join([]string{
"auth_service:",
" enabled: yes",
"",
" hosted_plugins:",
" enabled: yes",
" oauth_providers:",
" slack:",
" client_id: " + clientIDFile,
" client_secret: " + clientSecretFile,
}, "\n"),
readErr: require.NoError,
applyErr: require.NoError,
assert: func(t *testing.T, p servicecfg.HostedPluginsConfig) {
require.True(t, p.Enabled)
require.NotNil(t, p.OAuthProviders.SlackCredentials)
require.Equal(t, "foo", p.OAuthProviders.SlackCredentials.ClientID)
require.Equal(t, "bar", p.OAuthProviders.SlackCredentials.ClientSecret)
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
conf, err := ReadConfig(bytes.NewBufferString(tc.config))
tc.readErr(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
tc.applyErr(t, err)
if tc.assert != nil {
tc.assert(t, cfg.Auth.HostedPlugins)
}
})
}
}
func TestApplyDiscoveryConfig(t *testing.T) {
t.Parallel()
tests := []struct {
name string
discoveryConfig Discovery
expectedDiscovery servicecfg.DiscoveryConfig
}{
{
name: "no matchers",
discoveryConfig: Discovery{},
expectedDiscovery: servicecfg.DiscoveryConfig{
Enabled: true,
},
},
{
name: "azure matchers",
discoveryConfig: Discovery{
AzureMatchers: []AzureMatcher{
{
Types: []string{"aks", "vm"},
Subscriptions: []string{"abcd"},
InstallParams: &InstallParams{
JoinParams: JoinParams{
TokenName: "azure-token",
Method: "azure",
},
ScriptName: "default-installer",
PublicProxyAddr: "proxy.example.com",
Azure: &AzureInstallParams{
ClientID: "abcd1234",
},
},
},
},
},
expectedDiscovery: servicecfg.DiscoveryConfig{
Enabled: true,
AzureMatchers: []types.AzureMatcher{
{
Subscriptions: []string{"abcd"},
Types: []string{"aks", "vm"},
Params: &types.InstallerParams{
JoinMethod: "azure",
JoinToken: "azure-token",
ScriptName: "default-installer",
PublicProxyAddr: "proxy.example.com",
Azure: &types.AzureInstallerParams{
ClientID: "abcd1234",
},
},
Regions: []string{"*"},
ResourceTags: types.Labels{"*": []string{"*"}},
ResourceGroups: []string{"*"},
},
},
},
},
{
name: "azure matchers no installer",
discoveryConfig: Discovery{
AzureMatchers: []AzureMatcher{
{
Types: []string{"aks"},
Subscriptions: []string{"abcd"},
},
},
},
expectedDiscovery: servicecfg.DiscoveryConfig{
Enabled: true,
AzureMatchers: []types.AzureMatcher{
{
Subscriptions: []string{"abcd"},
Types: []string{"aks"},
Regions: []string{"*"},
ResourceTags: types.Labels{"*": []string{"*"}},
ResourceGroups: []string{"*"},
},
},
},
},
{
name: "tag matchers",
discoveryConfig: Discovery{
AccessGraph: &AccessGraphSync{
AWS: []AccessGraphAWSSync{
{
Regions: []string{"us-west-2", "us-east-1"},
AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
},
},
},
expectedDiscovery: servicecfg.DiscoveryConfig{
Enabled: true,
AccessGraph: &types.AccessGraphSync{
AWS: []*types.AccessGraphAWSSync{
{
Regions: []string{"us-west-2", "us-east-1"},
AssumeRole: &types.AssumeRole{
RoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
},
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fc, err := ReadConfig(bytes.NewBufferString(NoServicesConfigString))
require.NoError(t, err)
fc.Discovery = tc.discoveryConfig
fc.Discovery.EnabledFlag = "yes"
cfg := servicecfg.MakeDefaultConfig()
require.NoError(t, applyDiscoveryConfig(fc, cfg))
require.Equal(t, tc.expectedDiscovery, cfg.Discovery)
})
}
}
func TestApplyOktaConfig(t *testing.T) {
t.Parallel()
tests := []struct {
desc string
createTokenFile bool
oktaConfig Okta
expectedOkta servicecfg.OktaConfig
errAssertionFunc require.ErrorAssertionFunc
}{
{
desc: "valid config (access list sync defaults to false)",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
},
expectedOkta: servicecfg.OktaConfig{
Enabled: true,
APIEndpoint: "https://test-endpoint",
SyncSettings: servicecfg.OktaSyncSettings{
SyncAccessLists: false,
},
},
errAssertionFunc: require.NoError,
},
{
desc: "valid config (access list sync enabled)",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
Sync: OktaSync{
SyncAccessListsFlag: "yes",
DefaultOwners: []string{"owner1"},
},
},
expectedOkta: servicecfg.OktaConfig{
Enabled: true,
APIEndpoint: "https://test-endpoint",
SyncSettings: servicecfg.OktaSyncSettings{
SyncAccessLists: true,
DefaultOwners: []string{"owner1"},
},
},
errAssertionFunc: require.NoError,
},
{
desc: "valid config (access list sync with filters)",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
Sync: OktaSync{
SyncAccessListsFlag: "yes",
DefaultOwners: []string{"owner1"},
GroupFilters: []string{
"group*",
"^admin-.*$",
},
AppFilters: []string{
"app*",
"^admin-.*$",
},
},
},
expectedOkta: servicecfg.OktaConfig{
Enabled: true,
APIEndpoint: "https://test-endpoint",
SyncSettings: servicecfg.OktaSyncSettings{
SyncAccessLists: true,
DefaultOwners: []string{"owner1"},
GroupFilters: []string{
"group*",
"^admin-.*$",
},
AppFilters: []string{
"app*",
"^admin-.*$",
},
},
},
errAssertionFunc: require.NoError,
},
{
desc: "empty URL",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter("okta_service is enabled but no api_endpoint is specified"))
},
},
{
desc: "bad url",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: `bad%url`,
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter(`malformed URL bad%%url`))
},
},
{
desc: "no host",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: `http://`,
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter("api_endpoint has no host"))
},
},
{
desc: "no scheme",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: `//hostname`,
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter("api_endpoint has no scheme"))
},
},
{
desc: "empty file",
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter("okta_service is enabled but no api_token_path is specified"))
},
},
{
desc: "bad file",
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
APITokenPath: "/non-existent/path",
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter("error trying to find file %s", i...))
},
},
{
desc: "no default owners",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
Sync: OktaSync{
SyncAccessListsFlag: "yes",
},
},
errAssertionFunc: func(tt require.TestingT, err error, i ...any) {
require.ErrorIs(t, err, trace.BadParameter("default owners must be set when access list import is enabled"))
},
},
{
desc: "bad group filter",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
Sync: OktaSync{
SyncAccessListsFlag: "yes",
DefaultOwners: []string{"owner1"},
GroupFilters: []string{
"^admin-.[[[*$",
},
},
},
errAssertionFunc: func(t require.TestingT, err error, i ...any) {
require.ErrorContains(t, err, "error parsing group filter: ^admin-.[[[*$")
},
},
{
desc: "bad app filter",
createTokenFile: true,
oktaConfig: Okta{
Service: Service{
EnabledFlag: "yes",
},
APIEndpoint: "https://test-endpoint",
Sync: OktaSync{
SyncAccessListsFlag: "yes",
DefaultOwners: []string{"owner1"},
AppFilters: []string{
"^admin-.[[[*$",
},
},
},
errAssertionFunc: func(t require.TestingT, err error, i ...any) {
require.ErrorContains(t, err, "error parsing app filter: ^admin-.[[[*$")
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
fc, err := ReadConfig(bytes.NewBufferString(NoServicesConfigString))
require.NoError(t, err)
expectedOkta := test.expectedOkta
fc.Okta = test.oktaConfig
if test.createTokenFile {
file, err := os.CreateTemp("", "")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.Remove(file.Name()))
})
fc.Okta.APITokenPath = file.Name()
expectedOkta.APITokenPath = file.Name()
}
cfg := servicecfg.MakeDefaultConfig()
err = applyOktaConfig(fc, cfg)
test.errAssertionFunc(t, err, fc.Okta.APITokenPath)
if err == nil {
require.Equal(t, expectedOkta, cfg.Okta)
}
})
}
}
func TestApplyKubeConfig(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputFileConfig Kube
wantServiceConfig servicecfg.KubeConfig
wantError bool
}{
{
name: "invalid listener address",
inputFileConfig: Kube{
Service: Service{
ListenAddress: "0.0.0.0:a",
},
KubeconfigFile: "path-to-kubeconfig",
},
wantError: true,
},
{
name: "assume_role_arn is not supported",
inputFileConfig: Kube{
Service: Service{
ListenAddress: "0.0.0.0:8888",
},
KubeconfigFile: "path-to-kubeconfig",
ResourceMatchers: []ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"a": {"b"}},
AWS: ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/KubeAccess",
ExternalID: "externalID123",
},
},
},
},
wantError: false,
wantServiceConfig: servicecfg.KubeConfig{
ListenAddr: utils.MustParseAddr("0.0.0.0:8888"),
KubeconfigPath: "path-to-kubeconfig",
ResourceMatchers: []services.ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"a": {"b"}},
AWS: services.ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/KubeAccess",
ExternalID: "externalID123",
},
},
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
},
},
},
{
name: "valid",
inputFileConfig: Kube{
Service: Service{
ListenAddress: "0.0.0.0:8888",
},
PublicAddr: apiutils.Strings{"example.com", "example.with.port.com:4444"},
KubeconfigFile: "path-to-kubeconfig",
KubeClusterName: "kube-name",
ResourceMatchers: []ResourceMatcher{{
Labels: map[string]apiutils.Strings{"a": {"b"}},
}},
StaticLabels: map[string]string{
"env": "dev",
"product": "test",
},
DynamicLabels: []CommandLabel{{
Name: "hostname",
Command: []string{"hostname"},
Period: time.Hour,
}},
},
wantServiceConfig: servicecfg.KubeConfig{
ListenAddr: utils.MustParseAddr("0.0.0.0:8888"),
PublicAddrs: []utils.NetAddr{*utils.MustParseAddr("example.com:3026"), *utils.MustParseAddr("example.with.port.com:4444")},
KubeconfigPath: "path-to-kubeconfig",
KubeClusterName: "kube-name",
ResourceMatchers: []services.ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"a": {"b"}},
},
},
StaticLabels: map[string]string{
"env": "dev",
"product": "test",
},
DynamicLabels: services.CommandLabels{
"hostname": &types.CommandLabelV2{
Period: types.Duration(time.Hour),
Command: []string{"hostname"},
},
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fc, err := ReadConfig(bytes.NewBufferString(NoServicesConfigString))
require.NoError(t, err)
fc.Kube = test.inputFileConfig
fc.Kube.EnabledFlag = "yes"
cfg := servicecfg.MakeDefaultConfig()
err = applyKubeConfig(fc, cfg)
if test.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.wantServiceConfig, cfg.Kube)
}
})
}
}
func TestGetInstallerProxyAddr(t *testing.T) {
t.Parallel()
tests := []struct {
name string
installParams *InstallParams
fc *FileConfig
expectedProxyAddr string
}{
{
name: "empty",
fc: &FileConfig{},
expectedProxyAddr: "",
},
{
name: "explicit proxy addr",
installParams: &InstallParams{
PublicProxyAddr: "explicit.example.com",
},
fc: &FileConfig{
Global: Global{
ProxyServer: "proxy.example.com",
},
},
expectedProxyAddr: "explicit.example.com",
},
{
name: "proxy server",
fc: &FileConfig{
Global: Global{
ProxyServer: "proxy.example.com",
},
},
expectedProxyAddr: "proxy.example.com",
},
{
name: "local proxy service",
fc: &FileConfig{
Global: Global{
AuthServer: "auth.example.com",
},
Proxy: Proxy{
Service: Service{
EnabledFlag: "yes",
},
PublicAddr: apiutils.Strings{"proxy.example.com"},
},
},
expectedProxyAddr: "proxy.example.com",
},
{
name: "v1/v2 auth servers",
fc: &FileConfig{
Version: "v2",
Global: Global{
AuthServers: []string{"proxy.example.com"},
},
},
expectedProxyAddr: "proxy.example.com",
},
{
name: "auth server",
fc: &FileConfig{
Global: Global{
AuthServer: "auth.example.com",
},
},
expectedProxyAddr: "auth.example.com",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expectedProxyAddr, getInstallerProxyAddr(tc.installParams, tc.fc))
})
}
}
func TestDiscoveryConfig(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
mutate func(cfgMap)
expectError require.ErrorAssertionFunc
expectEnabled require.BoolAssertionFunc
expectedTotalMatchers int
expectedAWSMatchers []types.AWSMatcher
expectedAzureMatchers []types.AzureMatcher
expectedGCPMatchers []types.GCPMatcher
}{
{
desc: "default",
mutate: func(cfgMap) {},
expectError: require.NoError,
expectEnabled: require.False,
},
{
desc: "GCP section without project_ids",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
},
}
},
},
{
desc: "GCP section is filled with defaults",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
"project_ids": []string{"p1", "p2"},
},
}
},
expectedGCPMatchers: []types.GCPMatcher{{
Types: []string{"gke"},
Locations: []string{"*"},
Labels: map[string]apiutils.Strings{
"*": []string{"*"},
},
ProjectIDs: []string{"p1", "p2"},
}},
},
{
desc: "GCP section is filled",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
"locations": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"project_ids": []string{"p1", "p2"},
},
}
},
expectedGCPMatchers: []types.GCPMatcher{{
Types: []string{"gke"},
Locations: []string{"eucentral1"},
Labels: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
ProjectIDs: []string{"p1", "p2"},
}},
},
{
desc: "GCP section is filled with wildcard project ids",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
"locations": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"project_ids": []string{"*"},
},
}
},
expectedGCPMatchers: []types.GCPMatcher{{
Types: []string{"gke"},
Locations: []string{"eucentral1"},
Labels: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
ProjectIDs: []string{"*"},
}},
},
{
desc: "GCP section mixes wildcard and specific project ids",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gke"},
"locations": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"project_ids": []string{"p1", "*"},
},
}
},
},
{
desc: "GCP section is filled with installer",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["gcp"] = []cfgMap{
{
"types": []string{"gce"},
"locations": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"project_ids": []string{"p1", "p2"},
"service_accounts": []string{"[email protected]", "[email protected]"},
},
}
cfg["version"] = "v3"
cfg["teleport"].(cfgMap)["proxy_server"] = "example.com"
cfg["proxy_service"] = cfgMap{
"enabled": "no",
}
},
expectedGCPMatchers: []types.GCPMatcher{{
Types: []string{"gce"},
Locations: []string{"eucentral1"},
Labels: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
ProjectIDs: []string{"p1", "p2"},
ServiceAccounts: []string{"[email protected]", "[email protected]"},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodGCP,
JoinToken: types.GCPInviteTokenName,
ScriptName: installers.InstallerScriptName,
PublicProxyAddr: "example.com",
},
}},
},
{
desc: "Azure section is filled with defaults (aks)",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["azure"] = []cfgMap{
{
"types": []string{"aks"},
},
}
},
expectedAzureMatchers: []types.AzureMatcher{{
Types: []string{"aks"},
Regions: []string{"*"},
ResourceTags: map[string]apiutils.Strings{
"*": []string{"*"},
},
Subscriptions: []string{"*"},
ResourceGroups: []string{"*"},
}},
},
{
desc: "Azure section is filled with values",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["azure"] = []cfgMap{
{
"types": []string{"aks"},
"regions": []string{"eucentral1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"subscriptions": []string{"sub1", "sub2"},
"resource_groups": []string{"group1", "group2"},
},
}
},
expectedAzureMatchers: []types.AzureMatcher{{
Types: []string{"aks"},
Regions: []string{"eucentral1"},
ResourceTags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Subscriptions: []string{"sub1", "sub2"},
ResourceGroups: []string{"group1", "group2"},
}},
},
{
desc: "AWS section is filled with defaults",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"eu-central-1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
expectedAWSMatchers: []types.AWSMatcher{{
Types: []string{"ec2"},
Regions: []string{"eu-central-1"},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodIAM,
JoinToken: types.IAMInviteTokenName,
SSHDConfig: "/etc/ssh/sshd_config",
ScriptName: installers.InstallerScriptName,
InstallTeleport: true,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_SCRIPT,
},
SSM: &types.AWSSSM{DocumentName: types.AWSInstallerDocument},
}},
},
{
desc: "AWS section is filled using the example config in docs",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"us-east-1", "us-west-1"},
"install": map[string]map[string]string{
"join_params": {
"token_name": "aws-discovery-iam-token",
"method": "iam",
},
},
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
expectedAWSMatchers: []types.AWSMatcher{{
Types: []string{"ec2"},
Regions: []string{"us-east-1", "us-west-1"},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodIAM,
JoinToken: types.IAMInviteTokenName,
SSHDConfig: "/etc/ssh/sshd_config",
ScriptName: installers.InstallerScriptName,
InstallTeleport: true,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_SCRIPT,
},
SSM: &types.AWSSSM{DocumentName: types.AWSInstallerDocument},
}},
},
{
desc: "AWS section is filled with custom configs",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"eu-central-1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"install": cfgMap{
"join_params": cfgMap{
"token_name": "hello-iam-a-token",
"method": "iam",
},
"script_name": "installer-custom",
},
"ssm": cfgMap{
"document_name": "hello_document",
},
"assume_role_arn": "arn:aws:iam::123456789012:role/DBDiscoverer",
"external_id": "externalID123",
},
}
},
expectedAWSMatchers: []types.AWSMatcher{{
Types: []string{"ec2"},
Regions: []string{"eu-central-1"},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodIAM,
JoinToken: "hello-iam-a-token",
SSHDConfig: "/etc/ssh/sshd_config",
ScriptName: "installer-custom",
InstallTeleport: true,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_SCRIPT,
},
SSM: &types.AWSSSM{DocumentName: "hello_document"},
AssumeRole: &types.AssumeRole{
RoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
}},
},
{
desc: "AWS section with eice enroll mode",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"eu-central-1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"install": cfgMap{
"join_params": cfgMap{
"token_name": "hello-iam-a-token",
"method": "iam",
},
"script_name": "installer-custom",
"enroll_mode": "eice",
},
"ssm": cfgMap{
"document_name": "hello_document",
},
"assume_role_arn": "arn:aws:iam::123456789012:role/DBDiscoverer",
"external_id": "externalID123",
"integration": "my-integration",
},
}
},
expectedAWSMatchers: []types.AWSMatcher{{
Types: []string{"ec2"},
Regions: []string{"eu-central-1"},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodIAM,
JoinToken: "hello-iam-a-token",
SSHDConfig: "/etc/ssh/sshd_config",
ScriptName: "installer-custom",
InstallTeleport: true,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_EICE,
},
SSM: &types.AWSSSM{DocumentName: "hello_document"},
Integration: "my-integration",
AssumeRole: &types.AssumeRole{
RoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
ExternalID: "externalID123",
},
}},
},
{
desc: "AWS cannot use EICE mode without integration",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"eu-central-1"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"install": cfgMap{
"join_params": cfgMap{
"token_name": "hello-iam-a-token",
"method": "iam",
},
"script_name": "installer-custom",
"enroll_mode": "eice",
},
"ssm": cfgMap{
"document_name": "hello_document",
},
"assume_role_arn": "arn:aws:iam::123456789012:role/DBDiscoverer",
"external_id": "externalID123",
},
}
},
},
{
desc: "AWS section is filled with invalid region",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"*"},
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
},
{
desc: "AWS section is filled with invalid join method",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"install": cfgMap{
"join_params": cfgMap{
"token_name": "hello-iam-a-token",
"method": "token",
},
},
},
}
},
},
{
desc: "AWS section is filled with external_id but empty assume_role_arn",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"rds"},
"regions": []string{"us-west-1"},
"assume_role_arn": "",
"external_id": "externalid123",
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
},
{
desc: "AWS section is filled with external_id but empty assume_role_arn is ok for redshift serverless",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"redshift-serverless"},
"regions": []string{"us-west-1"},
"assume_role_arn": "",
"external_id": "externalid123",
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
expectedAWSMatchers: []types.AWSMatcher{{
Types: []string{"redshift-serverless"},
Regions: []string{"us-west-1"},
Tags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodIAM,
JoinToken: "aws-discovery-iam-token",
SSHDConfig: "/etc/ssh/sshd_config",
ScriptName: "default-installer",
InstallTeleport: true,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_SCRIPT,
},
SSM: &types.AWSSSM{DocumentName: "TeleportDiscoveryInstaller"},
AssumeRole: &types.AssumeRole{
RoleARN: "",
ExternalID: "externalid123",
},
}},
},
{
desc: "AWS section is filled with invalid assume_role_arn",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"rds"},
"regions": []string{"us-west-1"},
"assume_role_arn": "foobar",
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
},
{
desc: "AWS section is filled with assume_role_arn that is not an iam ARN",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"rds"},
"regions": []string{"us-west-1"},
"assume_role_arn": "arn:aws:sts::123456789012:federated-user/Alice",
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
},
},
{
desc: "AWS section is filled with no token",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["aws"] = []cfgMap{
{
"types": []string{"ec2"},
"regions": []string{"eu-west-1"},
"install": cfgMap{
"join_params": cfgMap{
"method": "iam",
},
},
},
}
},
expectedAWSMatchers: []types.AWSMatcher{{
Types: []string{"ec2"},
SSM: &types.AWSSSM{
DocumentName: types.AWSInstallerDocument,
},
Regions: []string{"eu-west-1"},
Tags: map[string]apiutils.Strings{"*": {"*"}},
Params: &types.InstallerParams{
JoinMethod: types.JoinMethodIAM,
JoinToken: types.IAMInviteTokenName,
ScriptName: installers.InstallerScriptName,
SSHDConfig: "/etc/ssh/sshd_config",
InstallTeleport: true,
EnrollMode: types.InstallParamEnrollMode_INSTALL_PARAM_ENROLL_MODE_SCRIPT,
},
}},
},
{
desc: "Azure section is filled with defaults (vm)",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["azure"] = []cfgMap{
{
"types": []string{"vm"},
"regions": []string{"westcentralus"},
"resource_groups": []string{"rg1"},
"subscriptions": []string{"88888888-8888-8888-8888-888888888888"},
"tags": cfgMap{
"discover_teleport": "yes",
},
},
}
cfg["version"] = "v3"
cfg["teleport"].(cfgMap)["proxy_server"] = "example.com"
cfg["proxy_service"] = cfgMap{
"enabled": "no",
}
},
expectedAzureMatchers: []types.AzureMatcher{{
Types: []string{"vm"},
Regions: []string{"westcentralus"},
ResourceGroups: []string{"rg1"},
Subscriptions: []string{"88888888-8888-8888-8888-888888888888"},
ResourceTags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: "azure",
JoinToken: "azure-discovery-token",
ScriptName: "default-installer",
PublicProxyAddr: "example.com",
Azure: &types.AzureInstallerParams{},
},
}},
},
{
desc: "Azure section is filled with custom config",
expectError: require.NoError,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["azure"] = []cfgMap{
{
"types": []string{"vm"},
"regions": []string{"westcentralus"},
"resource_groups": []string{"rg1"},
"subscriptions": []string{"88888888-8888-8888-8888-888888888888"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"install": cfgMap{
"join_params": cfgMap{
"token_name": "custom-azure-token",
"method": "azure",
},
"script_name": "custom-installer",
"public_proxy_addr": "teleport.example.com",
},
},
}
},
expectedAzureMatchers: []types.AzureMatcher{{
Types: []string{"vm"},
Regions: []string{"westcentralus"},
ResourceGroups: []string{"rg1"},
Subscriptions: []string{"88888888-8888-8888-8888-888888888888"},
ResourceTags: map[string]apiutils.Strings{
"discover_teleport": []string{"yes"},
},
Params: &types.InstallerParams{
JoinMethod: "azure",
JoinToken: "custom-azure-token",
ScriptName: "custom-installer",
PublicProxyAddr: "teleport.example.com",
Azure: &types.AzureInstallerParams{},
},
}},
},
{
desc: "Azure section is filled with invalid join method",
expectError: require.Error,
expectEnabled: require.True,
mutate: func(cfg cfgMap) {
cfg["discovery_service"].(cfgMap)["enabled"] = "yes"
cfg["discovery_service"].(cfgMap)["azure"] = []cfgMap{
{
"types": []string{"vm"},
"regions": []string{"westcentralus"},
"resource_groups": []string{"rg1"},
"subscriptions": []string{"88888888-8888-8888-8888-888888888888"},
"tags": cfgMap{
"discover_teleport": "yes",
},
"install": cfgMap{
"join_params": cfgMap{
"token_name": "custom-azure-token",
"method": "token",
},
},
},
}
},
},
}
for _, testCase := range testCases {
t.Run(testCase.desc, func(t *testing.T) {
text := bytes.NewBuffer(editConfig(t, testCase.mutate))
fc, err := ReadConfig(text)
require.NoError(t, err)
cfg := servicecfg.MakeDefaultConfig()
err = ApplyFileConfig(fc, cfg)
testCase.expectError(t, err)
if cfg == nil {
return
}
testCase.expectEnabled(t, cfg.Discovery.Enabled)
require.Equal(t, testCase.expectedAWSMatchers, cfg.Discovery.AWSMatchers)
require.Equal(t, testCase.expectedAzureMatchers, cfg.Discovery.AzureMatchers)
require.Equal(t, testCase.expectedGCPMatchers, cfg.Discovery.GCPMatchers)
})
}
}
// TestProxyUntrustedCert tests that configuring a Proxy Service with an HTTPS
// cert signed by an untrusted cert authority is an error, and returns a helpful
// error message.
func TestProxyUntrustedCert(t *testing.T) {
t.Parallel()
caPriv, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err)
// Generate a CA for the test that will not be trusted.
caCert, err := tlsca.GenerateSelfSignedCAWithSigner(
caPriv,
pkix.Name{
CommonName: "CA",
Organization: []string{"teleport"},
}, nil, defaults.CATTL)
require.NoError(t, err)
ca, err := tlsca.FromCertAndSigner(caCert, caPriv)
require.NoError(t, err)
// Generate a leaf cert signed by the untrusted CA.
leafPriv, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
require.NoError(t, err)
leafCert, err := ca.GenerateCertificate(tlsca.CertificateRequest{
PublicKey: leafPriv.Public(),
Subject: pkix.Name{CommonName: "leaf"},
NotAfter: time.Now().Add(time.Hour),
})
require.NoError(t, err)
leafPrivPem, err := keys.MarshalPrivateKey(leafPriv)
require.NoError(t, err)
certDir := t.TempDir()
certPath := filepath.Join(certDir, "leaf.crt")
keyPath := filepath.Join(certDir, "leaf.key")
err = os.WriteFile(certPath, leafCert, 0600)
require.NoError(t, err)
err = os.WriteFile(keyPath, leafPrivPem, 0600)
require.NoError(t, err)
fc := &FileConfig{
Proxy: Proxy{
KeyPairs: []KeyPair{
{
PrivateKey: keyPath,
Certificate: certPath,
},
},
},
}
cfg := &servicecfg.Config{}
err = applyProxyConfig(fc, cfg)
require.Error(t, err)
require.ErrorContains(t, err, certPath)
require.ErrorContains(t, err, proxyUntrustedTLSCertErrMsg)
// We can't test that writing the CA cert to file and setting SSL_CERT_FILE
// would fix this error, because:
// - the system root certs are loaded exactly once and cached
// - it only works on linux
}
func TestDebugServiceConfig(t *testing.T) {
for name, tc := range map[string]struct {
configFile string
commandLineFlags *CommandLineFlags
expectDebugServiceEnabled bool
}{
"enabled by default": {configFile: "", expectDebugServiceEnabled: true},
"disabled by commandline": {
configFile: "",
commandLineFlags: &CommandLineFlags{DisableDebugService: true},
expectDebugServiceEnabled: false,
},
"disabled by configuration": {
configFile: `
debug_service:
enabled: "no"
`,
expectDebugServiceEnabled: false,
},
"commandline flag has priority over config file": {
configFile: `
debug_service:
enabled: "yes"
`,
commandLineFlags: &CommandLineFlags{DisableDebugService: true},
expectDebugServiceEnabled: false,
},
} {
t.Run(name, func(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(filePath, []byte(tc.configFile), 0o777))
if tc.commandLineFlags == nil {
tc.commandLineFlags = &CommandLineFlags{}
}
tc.commandLineFlags.ConfigFile = filePath
conf := servicecfg.MakeDefaultConfig()
require.NoError(t, Configure(tc.commandLineFlags, conf, false))
require.Equal(t, tc.expectDebugServiceEnabled, conf.DebugService.Enabled)
})
}
}
func TestSignatureAlgorithmSuite(t *testing.T) {
for desc, tc := range map[string]struct {
fips bool
hsm bool
cloud bool
configuredSuite types.SignatureAlgorithmSuite
expectErr string
}{
"empty": {},
"hsm with no configured suite": {
hsm: true,
},
"hsm with balanced-v1": {
hsm: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
expectErr: `configured "signature_algorithm_suite" is unsupported when "ca_key_params" configures an HSM or KMS`,
},
"hsm with hsm-v1": {
hsm: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
},
"hsm with fips-v1": {
hsm: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1,
},
"hsm with legacy": {
hsm: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_LEGACY,
},
"fips with no configured suite": {
fips: true,
},
"fips with balanced-v1": {
fips: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
expectErr: `non-FIPS compliant authentication setting: "signature_algorithm_suite" must be "fips-v1" or "legacy"`,
},
"fips with fips-v1": {
fips: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1,
},
"fips with legacy": {
fips: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_LEGACY,
},
"cloud with no configured suite": {
cloud: true,
},
"cloud with balanced-v1": {
cloud: true,
configuredSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
expectErr: `configured "signature_algorithm_suite" is unsupported in Teleport Cloud`,
},
} {
t.Run(desc, func(t *testing.T) {
modulestest.SetTestModules(t, modulestest.Modules{
TestFeatures: modules.Features{
Cloud: tc.cloud,
},
})
clf := &CommandLineFlags{
FIPS: tc.fips,
}
cfg := servicecfg.MakeDefaultConfig()
if tc.fips {
servicecfg.ApplyFIPSDefaults(cfg)
}
if tc.hsm {
cfg.Auth.KeyStore.AWSKMS = &servicecfg.AWSKMSConfig{
AWSAccount: "123456789012",
AWSRegion: "us-west-2",
}
} else {
cfg.Auth.KeyStore = servicecfg.KeystoreConfig{}
}
if tc.configuredSuite != types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED {
cfg.Auth.Preference.SetOrigin(types.OriginConfigFile)
cfg.Auth.Preference.SetSignatureAlgorithmSuite(tc.configuredSuite)
}
err := Configure(clf, cfg, false)
if tc.expectErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectErr)
return
}
require.NoError(t, err)
})
}
}