mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:27:24 +08:00
* Move configuration from lib/service to lib/service/servicecfg The new servicecfg package will hold only configuration for services. This will allow other packages (like tctl and tsh) to depend on servicecfg without pulling in all of lib/service (which has a number of platform-specific details). This is the first step towards being able to build tctl for Windows. * Move PAM and BPF config into servicecfg This breaks a compile-time dependency on BPF/PAM for tctl.
3556 lines
96 KiB
Go
3556 lines
96 KiB
Go
/*
|
|
Copyright 2015-2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"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"
|
|
apiutils "github.com/gravitational/teleport/api/utils"
|
|
"github.com/gravitational/teleport/lib"
|
|
"github.com/gravitational/teleport/lib/auth/keystore"
|
|
"github.com/gravitational/teleport/lib/backend"
|
|
"github.com/gravitational/teleport/lib/backend/lite"
|
|
"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/service/servicecfg"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
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) {
|
|
utils.InitLoggerForTests()
|
|
|
|
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: "alice@example.com",
|
|
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, fc.Global.DataDir, defaults.DataDir)
|
|
require.Equal(t, fc.Logger.Severity, "INFO")
|
|
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 boolean options
|
|
// 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([]byte(fmt.Sprintf(`
|
|
teleport:
|
|
advertise_ip: 10.10.10.1
|
|
auth_service:
|
|
enabled: yes
|
|
disconnect_expired_cert: %v
|
|
`, tc.s))))
|
|
require.NoError(t, err, 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([]byte(fmt.Sprintf(`
|
|
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: "bolt",
|
|
},
|
|
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),
|
|
},
|
|
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"},
|
|
},
|
|
InstallParams: &InstallParams{
|
|
JoinParams: JoinParams{
|
|
TokenName: "aws-discovery-iam-token",
|
|
Method: "iam",
|
|
},
|
|
SSHDConfig: "/etc/ssh/sshd_config",
|
|
ScriptName: "default-installer",
|
|
},
|
|
SSM: AWSSSM{DocumentName: "TeleportDiscoveryInstaller"},
|
|
},
|
|
},
|
|
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"},
|
|
Tags: 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,
|
|
},
|
|
},
|
|
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{
|
|
"*": {"*"},
|
|
},
|
|
},
|
|
},
|
|
AWSMatchers: []AWSMatcher{
|
|
{
|
|
Types: []string{"rds"},
|
|
Regions: []string{"us-west-1", "us-east-1"},
|
|
Tags: map[string]apiutils.Strings{
|
|
"a": {"b"},
|
|
},
|
|
},
|
|
{
|
|
Types: []string{"rds"},
|
|
Regions: []string{"us-central-1"},
|
|
Tags: map[string]apiutils.Strings{
|
|
"c": {"d"},
|
|
},
|
|
},
|
|
},
|
|
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,
|
|
},
|
|
WindowsDesktop: WindowsDesktopService{
|
|
Service: Service{
|
|
EnabledFlag: "yes",
|
|
ListenAddress: "tcp://windows_desktop",
|
|
},
|
|
PublicAddr: apiutils.Strings([]string{"winsrv.example.com:3028", "no-port.winsrv.example.com"}),
|
|
Hosts: 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.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.Nil(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.Len(t, conf.CmdLabels, 0)
|
|
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.Nil(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:
|
|
- hmac-sha2-256-etm@openssh.com
|
|
`,
|
|
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 := fmt.Sprintf(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)
|
|
|
|
conf, err := ReadConfig(bytes.NewBufferString(fmt.Sprintf(
|
|
SmallConfigString,
|
|
authTokenPath,
|
|
caPinPath,
|
|
staticTokenPath,
|
|
pkcs11LibPath,
|
|
)))
|
|
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.ProvisionTokensFromV1([]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.Equal(t, true, cfg.Proxy.IdP.SAMLIdP.Enabled)
|
|
require.Equal(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.SecondFactorOptional,
|
|
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),
|
|
IDP: &types.IdPOptions{
|
|
SAML: &types.IdPSAMLOptions{
|
|
Enabled: types.NewBoolOption(true),
|
|
},
|
|
},
|
|
},
|
|
}, 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.AzureMatchers,
|
|
[]services.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.True(t, cfg.Kube.Enabled)
|
|
require.Empty(t, cmp.Diff(cfg.Kube.ResourceMatchers,
|
|
[]services.ResourceMatcher{
|
|
{
|
|
Labels: map[string]apiutils.Strings{
|
|
"*": {"*"},
|
|
},
|
|
},
|
|
},
|
|
))
|
|
require.Equal(t, cfg.Kube.KubeconfigPath, "/tmp/kubeconfig")
|
|
require.Empty(t, cmp.Diff(cfg.Kube.StaticLabels,
|
|
map[string]string{
|
|
"testKey": "testValue",
|
|
},
|
|
))
|
|
|
|
require.True(t, cfg.Discovery.Enabled)
|
|
require.Equal(t, cfg.Discovery.AWSMatchers[0].Regions, []string{"eu-central-1"})
|
|
require.Equal(t, cfg.Discovery.AWSMatchers[0].Types, []string{"ec2"})
|
|
require.Equal(t, cfg.Discovery.AWSMatchers[0].Params, services.InstallerParams{
|
|
InstallTeleport: true,
|
|
JoinMethod: "iam",
|
|
JoinToken: defaults.IAMInviteTokenName,
|
|
ScriptName: "default-installer",
|
|
SSHDConfig: defaults.SSHDConfigPath,
|
|
})
|
|
}
|
|
|
|
// 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.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, cfg.Auth.ClusterName.GetClusterName(), "example.com")
|
|
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 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, cfg.Auth.StorageConfig.Type, lite.GetName())
|
|
require.Equal(t, cfg.Auth.StorageConfig.Params[defaults.BackendPath], "/var/lib/teleport/mybackend")
|
|
|
|
// 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 interface{}
|
|
}{
|
|
{
|
|
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 interface{}
|
|
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, conf.AuthToken, "xxxyyy")
|
|
require.Equal(t, conf.AdvertiseIP, "10.10.10.1:3022")
|
|
require.Equal(t, conf.PIDFile, "/var/run/teleport.pid")
|
|
|
|
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)
|
|
}
|
|
|
|
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 = "bolt"
|
|
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)
|
|
|
|
// 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"}},
|
|
},
|
|
}
|
|
|
|
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"},
|
|
Tags: 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,
|
|
},
|
|
}
|
|
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{"*": {"*"}},
|
|
},
|
|
}
|
|
conf.Databases.AWSMatchers = []AWSMatcher{
|
|
{
|
|
Types: []string{"rds"},
|
|
Regions: []string{"us-west-1", "us-east-1"},
|
|
Tags: map[string]apiutils.Strings{"a": {"b"}},
|
|
},
|
|
{
|
|
Types: []string{"rds"},
|
|
Regions: []string{"us-central-1"},
|
|
Tags: map[string]apiutils.Strings{"c": {"d"}},
|
|
},
|
|
}
|
|
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",
|
|
},
|
|
}
|
|
|
|
// 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"}),
|
|
Hosts: 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,
|
|
EnableProxyProtocol: true,
|
|
Kube: servicecfg.KubeProxyConfig{
|
|
Enabled: false,
|
|
},
|
|
Limiter: limiter.Config{
|
|
MaxConnections: defaults.LimiterMaxConnections,
|
|
MaxNumberOfUsers: 250,
|
|
},
|
|
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,
|
|
EnableProxyProtocol: true,
|
|
Kube: servicecfg.KubeProxyConfig{
|
|
Enabled: true,
|
|
},
|
|
Limiter: limiter.Config{
|
|
MaxConnections: defaults.LimiterMaxConnections,
|
|
MaxNumberOfUsers: 250,
|
|
},
|
|
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))
|
|
|
|
require.Empty(t, cmp.Diff(cfg.Proxy, tt.want, cmpopts.EquateEmpty()))
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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([]byte(fmt.Sprintf(
|
|
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
|
|
result string
|
|
}{
|
|
// 0 - no license
|
|
{
|
|
path: "",
|
|
result: filepath.Join(defaults.DataDir, defaults.LicenseFile),
|
|
},
|
|
// 1 - relative path
|
|
{
|
|
path: "lic.pem",
|
|
result: filepath.Join(defaults.DataDir, "lic.pem"),
|
|
},
|
|
// 2 - absolute path
|
|
{
|
|
path: "/etc/teleport/license",
|
|
result: "/etc/teleport/license",
|
|
},
|
|
}
|
|
|
|
cfg := servicecfg.MakeDefaultConfig()
|
|
require.Equal(t, filepath.Join(defaults.DataDir, defaults.LicenseFile), cfg.Auth.LicenseFile)
|
|
|
|
for _, tc := range testCases {
|
|
fc := new(FileConfig)
|
|
require.NoError(t, fc.CheckAndSetDefaults())
|
|
fc.Auth.LicenseFile = tc.path
|
|
err := ApplyFileConfig(fc, cfg)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.result, 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{"kube.example.com:443"}),
|
|
}},
|
|
want: servicecfg.KubeProxyConfig{
|
|
Enabled: true,
|
|
ListenAddr: *utils.MustParseAddr("0.0.0.0:8080"),
|
|
KubeconfigPath: "/tmp/kubeconfig",
|
|
PublicAddrs: []utils.NetAddr{*utils.MustParseAddr("kube.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{"kube.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,
|
|
EnableProxyProtocol: true,
|
|
Kube: servicecfg.KubeProxyConfig{
|
|
Enabled: true,
|
|
},
|
|
Limiter: limiter.Config{
|
|
MaxConnections: defaults.LimiterMaxConnections,
|
|
MaxNumberOfUsers: 250,
|
|
},
|
|
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,
|
|
EnableProxyProtocol: true,
|
|
WebAddr: *utils.MustParseAddr("0.0.0.0:9999"),
|
|
Kube: servicecfg.KubeProxyConfig{
|
|
Enabled: true,
|
|
},
|
|
Limiter: limiter.Config{
|
|
MaxConnections: defaults.LimiterMaxConnections,
|
|
MaxNumberOfUsers: 250,
|
|
},
|
|
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)
|
|
require.Empty(t, cmp.Diff(cfg.Proxy, tt.want, cmpopts.EquateEmpty()))
|
|
})
|
|
}
|
|
}
|
|
|
|
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.Hosts = []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.Hosts = []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.Hosts = []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.Hosts = []string{}
|
|
fc.WindowsDesktop.LDAP = LDAPConfig{
|
|
Addr: "",
|
|
}
|
|
},
|
|
},
|
|
{
|
|
desc: "OK - no hosts specified and ldap specified",
|
|
expectError: require.NoError,
|
|
mutate: func(fc *FileConfig) {
|
|
fc.WindowsDesktop.Hosts = []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: "OK - valid config",
|
|
expectError: require.NoError,
|
|
mutate: func(fc *FileConfig) {
|
|
fc.WindowsDesktop.EnabledFlag = "yes"
|
|
fc.WindowsDesktop.ListenAddress = "0.0.0.0:3028"
|
|
fc.WindowsDesktop.Hosts = []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
|
|
inComment string
|
|
outError bool
|
|
}{
|
|
{
|
|
inConfigString: `
|
|
app_service:
|
|
enabled: true
|
|
apps:
|
|
-
|
|
name: foo
|
|
public_addr: "foo.example.com"
|
|
uri: "http://127.0.0.1:8080"
|
|
resources:
|
|
- labels:
|
|
'*': '*'
|
|
`,
|
|
inComment: "config is valid",
|
|
outError: false,
|
|
},
|
|
{
|
|
inConfigString: `
|
|
app_service:
|
|
enabled: true
|
|
apps:
|
|
-
|
|
public_addr: "foo.example.com"
|
|
uri: "http://127.0.0.1:8080"
|
|
`,
|
|
inComment: "config is missing name",
|
|
outError: true,
|
|
},
|
|
{
|
|
inConfigString: `
|
|
app_service:
|
|
enabled: true
|
|
apps:
|
|
-
|
|
name: foo
|
|
uri: "http://127.0.0.1:8080"
|
|
`,
|
|
inComment: "config is valid",
|
|
outError: false,
|
|
},
|
|
{
|
|
inConfigString: `
|
|
app_service:
|
|
enabled: true
|
|
apps:
|
|
-
|
|
name: foo
|
|
public_addr: "foo.example.com"
|
|
`,
|
|
inComment: "config is missing internal address",
|
|
outError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
clf := CommandLineFlags{
|
|
ConfigString: base64.StdEncoding.EncodeToString([]byte(tt.inConfigString)),
|
|
}
|
|
cfg := servicecfg.MakeDefaultConfig()
|
|
|
|
err := Configure(&clf, cfg, false)
|
|
require.Equal(t, err != nil, tt.outError, tt.inComment)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
outApps []servicecfg.App
|
|
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 ...interface{}) {
|
|
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 ...interface{}) {
|
|
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 ...interface{}) {
|
|
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 ...interface{}) {
|
|
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 ...interface{}) {
|
|
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 ...interface{}) {
|
|
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 ...interface{}) {
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.ErrorContains(t, err, "application name \"-foo\" must be a valid DNS subdomain: https://goteleport.com/teleport/docs/application-access/#application-name")
|
|
},
|
|
},
|
|
{
|
|
desc: "missing uri",
|
|
inAppName: "foo",
|
|
outApps: nil,
|
|
requireError: func(t require.TestingT, err error, i ...interface{}) {
|
|
require.True(t, trace.IsBadParameter(err))
|
|
require.ErrorContains(t, err, "missing application \"foo\" URI")
|
|
},
|
|
},
|
|
}
|
|
|
|
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,
|
|
}
|
|
cfg := servicecfg.MakeDefaultConfig()
|
|
err := Configure(&clf, cfg, tt.inLegacyAppFlags)
|
|
tt.requireError(t, err)
|
|
require.Equal(t, tt.outApps, cfg.Apps.Apps)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDatabaseConfig(t *testing.T) {
|
|
tests := []struct {
|
|
inConfigString string
|
|
desc string
|
|
outError 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
|
|
`,
|
|
outError: "",
|
|
},
|
|
{
|
|
desc: "missing database name",
|
|
inConfigString: `
|
|
db_service:
|
|
enabled: true
|
|
databases:
|
|
- protocol: postgres
|
|
uri: localhost:5432
|
|
`,
|
|
outError: "empty database name",
|
|
},
|
|
{
|
|
desc: "unsupported database protocol",
|
|
inConfigString: `
|
|
db_service:
|
|
enabled: true
|
|
databases:
|
|
- name: foo
|
|
protocol: unknown
|
|
uri: localhost:5432
|
|
`,
|
|
outError: `unsupported database "foo" protocol`,
|
|
},
|
|
{
|
|
desc: "missing database uri",
|
|
inConfigString: `
|
|
db_service:
|
|
enabled: true
|
|
databases:
|
|
- name: foo
|
|
protocol: postgres
|
|
`,
|
|
outError: `database "foo" URI is empty`,
|
|
},
|
|
{
|
|
desc: "invalid database uri (missing port)",
|
|
inConfigString: `
|
|
db_service:
|
|
enabled: true
|
|
databases:
|
|
- name: foo
|
|
protocol: postgres
|
|
uri: 192.168.1.1
|
|
`,
|
|
outError: `invalid database "foo" address`,
|
|
},
|
|
}
|
|
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)
|
|
if tt.outError != "" {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.outError)
|
|
} else {
|
|
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
|
|
outError string
|
|
}{
|
|
{
|
|
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"},
|
|
},
|
|
},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "unsupported database protocol",
|
|
inFlags: CommandLineFlags{
|
|
DatabaseName: "foo",
|
|
DatabaseProtocol: "unknown",
|
|
DatabaseURI: "localhost:5432",
|
|
},
|
|
outError: `unsupported database "foo" protocol`,
|
|
},
|
|
{
|
|
desc: "missing database uri",
|
|
inFlags: CommandLineFlags{
|
|
DatabaseName: "foo",
|
|
DatabaseProtocol: defaults.ProtocolPostgres,
|
|
},
|
|
outError: `database "foo" URI is empty`,
|
|
},
|
|
{
|
|
desc: "invalid database uri (missing port)",
|
|
inFlags: CommandLineFlags{
|
|
DatabaseName: "foo",
|
|
DatabaseProtocol: defaults.ProtocolPostgres,
|
|
DatabaseURI: "localhost",
|
|
},
|
|
outError: `invalid database "foo" address`,
|
|
},
|
|
{
|
|
desc: "RDS database",
|
|
inFlags: CommandLineFlags{
|
|
DatabaseName: "rds",
|
|
DatabaseProtocol: defaults.ProtocolMySQL,
|
|
DatabaseURI: "localhost:3306",
|
|
DatabaseAWSRegion: "us-east-1",
|
|
},
|
|
outDatabase: servicecfg.Database{
|
|
Name: "rds",
|
|
Protocol: defaults.ProtocolMySQL,
|
|
URI: "localhost:3306",
|
|
AWS: servicecfg.DatabaseAWS{
|
|
Region: "us-east-1",
|
|
},
|
|
StaticLabels: map[string]string{
|
|
types.OriginLabel: types.OriginConfigFile,
|
|
},
|
|
DynamicLabels: services.CommandLabels{},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "Redshift database",
|
|
inFlags: CommandLineFlags{
|
|
DatabaseName: "redshift",
|
|
DatabaseProtocol: defaults.ProtocolPostgres,
|
|
DatabaseURI: "localhost:5432",
|
|
DatabaseAWSRegion: "us-east-1",
|
|
DatabaseAWSRedshiftClusterID: "redshift-cluster-1",
|
|
},
|
|
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",
|
|
},
|
|
},
|
|
StaticLabels: map[string]string{
|
|
types.OriginLabel: types.OriginConfigFile,
|
|
},
|
|
DynamicLabels: services.CommandLabels{},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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{
|
|
Mode: servicecfg.VerifyFull,
|
|
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: "localhost:1433",
|
|
DatabaseADKeytabFile: "/etc/keytab",
|
|
DatabaseADDomain: "EXAMPLE.COM",
|
|
DatabaseADSPN: "MSSQLSvc/sqlserver.example.com:1433",
|
|
},
|
|
outDatabase: servicecfg.Database{
|
|
Name: "sqlserver",
|
|
Protocol: defaults.ProtocolSQLServer,
|
|
URI: "localhost:1433",
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
AD: servicecfg.DatabaseAD{
|
|
KeytabFile: "/etc/keytab",
|
|
Krb5File: defaults.Krb5FilePath,
|
|
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",
|
|
},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
StaticLabels: map[string]string{
|
|
types.OriginLabel: types.OriginConfigFile,
|
|
},
|
|
DynamicLabels: services.CommandLabels{},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "AWS DynamoDB",
|
|
inFlags: CommandLineFlags{
|
|
DatabaseName: "ddb",
|
|
DatabaseProtocol: defaults.ProtocolDynamoDB,
|
|
DatabaseURI: "dynamodb.us-east-1.amazonaws.com",
|
|
DatabaseAWSAccountID: "123456789012",
|
|
DatabaseAWSExternalID: "12345678901234",
|
|
DatabaseAWSRegion: "us-east-1",
|
|
},
|
|
outDatabase: servicecfg.Database{
|
|
Name: "ddb",
|
|
Protocol: defaults.ProtocolDynamoDB,
|
|
URI: "dynamodb.us-east-1.amazonaws.com",
|
|
AWS: servicecfg.DatabaseAWS{
|
|
Region: "us-east-1",
|
|
AccountID: "123456789012",
|
|
ExternalID: "12345678901234",
|
|
},
|
|
StaticLabels: map[string]string{
|
|
types.OriginLabel: types.OriginConfigFile,
|
|
},
|
|
DynamicLabels: services.CommandLabels{},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
Mode: servicecfg.VerifyFull,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
config := servicecfg.MakeDefaultConfig()
|
|
err := Configure(&tt.inFlags, config, false)
|
|
if tt.outError != "" {
|
|
require.Contains(t, err.Error(), tt.outError)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t,
|
|
config.Databases.Databases,
|
|
[]servicecfg.Database{tt.outDatabase},
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTextFormatter(t *testing.T) {
|
|
tests := []struct {
|
|
comment string
|
|
formatConfig []string
|
|
assertErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
comment: "invalid key (does not exist)",
|
|
formatConfig: []string{"level", "invalid key"},
|
|
assertErr: require.Error,
|
|
},
|
|
{
|
|
comment: "valid keys and formatting",
|
|
formatConfig: []string{"level", "component", "timestamp"},
|
|
assertErr: require.NoError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.comment, func(t *testing.T) {
|
|
formatter := &utils.TextFormatter{
|
|
ExtraFields: tt.formatConfig,
|
|
}
|
|
tt.assertErr(t, formatter.CheckAndSetDefaults())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSONFormatter(t *testing.T) {
|
|
tests := []struct {
|
|
comment string
|
|
extraFields []string
|
|
assertErr require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
comment: "invalid key (does not exist)",
|
|
extraFields: []string{"level", "invalid key"},
|
|
assertErr: require.Error,
|
|
},
|
|
{
|
|
comment: "valid keys and formatting",
|
|
extraFields: []string{"level", "caller", "component", "timestamp"},
|
|
assertErr: require.NoError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.comment, func(t *testing.T) {
|
|
formatter := &utils.JSONFormatter{
|
|
ExtraFields: tt.extraFields,
|
|
}
|
|
tt.assertErr(t, formatter.CheckAndSetDefaults())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTLSCert(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
tmpCA := path.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 keystore.Config
|
|
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: keystore.Config{
|
|
PKCS11: keystore.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: keystore.Config{
|
|
PKCS11: keystore.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 gcp config",
|
|
auth: Auth{
|
|
CAKeyParams: &CAKeyParams{
|
|
GoogleCloudKMS: &GoogleCloudKMS{
|
|
KeyRing: "/projects/my-project/locations/global/keyRings/my-keyring",
|
|
ProtectionLevel: "HSM",
|
|
},
|
|
},
|
|
},
|
|
want: keystore.Config{
|
|
GCPKMS: keystore.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 TestApplyFileConfig_deviceTrustMode_errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
buildType string
|
|
deviceTrust *DeviceTrust
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "ok: OSS Mode=off",
|
|
buildType: modules.BuildOSS,
|
|
deviceTrust: &DeviceTrust{
|
|
Mode: constants.DeviceTrustModeOff,
|
|
},
|
|
},
|
|
{
|
|
name: "nok: OSS Mode=required",
|
|
buildType: modules.BuildOSS,
|
|
deviceTrust: &DeviceTrust{
|
|
Mode: constants.DeviceTrustModeRequired,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ok: Enterprise Mode=required",
|
|
buildType: modules.BuildEnterprise,
|
|
deviceTrust: &DeviceTrust{
|
|
Mode: constants.DeviceTrustModeRequired,
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
modules.SetTestModules(t, &modules.TestModules{
|
|
TestBuildType: test.buildType,
|
|
})
|
|
|
|
defaultCfg := servicecfg.MakeDefaultConfig()
|
|
err := ApplyFileConfig(&FileConfig{
|
|
Auth: Auth{
|
|
Service: Service{
|
|
EnabledFlag: "yes",
|
|
},
|
|
Authentication: &AuthenticationConfig{
|
|
DeviceTrust: test.deviceTrust,
|
|
},
|
|
},
|
|
}, defaultCfg)
|
|
if test.wantErr {
|
|
assert.Error(t, err, "ApplyFileConfig mismatch")
|
|
} else {
|
|
assert.NoError(t, err, "ApplyFileConfig mismatch")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthHostedPlugins(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
badParameter := func(t require.TestingT, err error, msgAndArgs ...interface{}) {
|
|
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 ...interface{}) {
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, os.ErrNotExist), `expected "does not exist", but got %v`)
|
|
}
|
|
|
|
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 disabled 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.False(t, p.Enabled)
|
|
},
|
|
},
|
|
{
|
|
desc: "Plugins enabled but zero providers defined",
|
|
config: strings.Join([]string{
|
|
"auth_service:",
|
|
" enabled: yes",
|
|
" hosted_plugins:",
|
|
" enabled: yes",
|
|
}, "\n"),
|
|
readErr: require.NoError,
|
|
applyErr: badParameter,
|
|
},
|
|
{
|
|
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.Slack)
|
|
require.Equal(t, "foo", p.OAuthProviders.Slack.ID)
|
|
require.Equal(t, "bar", p.OAuthProviders.Slack.Secret)
|
|
},
|
|
},
|
|
}
|
|
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedDiscovery: servicecfg.DiscoveryConfig{
|
|
Enabled: true,
|
|
AzureMatchers: []services.AzureMatcher{
|
|
{
|
|
Subscriptions: []string{"abcd"},
|
|
Types: []string{"aks", "vm"},
|
|
Params: services.InstallerParams{
|
|
JoinMethod: "azure",
|
|
JoinToken: "azure-token",
|
|
ScriptName: "default-installer",
|
|
PublicProxyAddr: "proxy.example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "azure matchers no installer",
|
|
discoveryConfig: Discovery{
|
|
AzureMatchers: []AzureMatcher{
|
|
{
|
|
Types: []string{"aks"},
|
|
Subscriptions: []string{"abcd"},
|
|
},
|
|
},
|
|
},
|
|
expectedDiscovery: servicecfg.DiscoveryConfig{
|
|
Enabled: true,
|
|
AzureMatchers: []services.AzureMatcher{
|
|
{
|
|
Subscriptions: []string{"abcd"},
|
|
Types: []string{"aks"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|