mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-25 18:59:51 +08:00
* Write status on boundkeypair when created from apply-on-startup * whitespace changes * Update to not reset status * Add check for defaulted recovery policy * Add additional check for compare failure
2897 lines
106 KiB
Go
2897 lines
106 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coreos/go-semver/semver"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/google/uuid"
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1"
|
|
dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
|
|
healthcheckconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1"
|
|
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/types/backendinfo"
|
|
"github.com/gravitational/teleport/api/types/label"
|
|
"github.com/gravitational/teleport/api/types/vnet"
|
|
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
|
|
"github.com/gravitational/teleport/entitlements"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/authcatest"
|
|
"github.com/gravitational/teleport/lib/auth/authtest"
|
|
"github.com/gravitational/teleport/lib/auth/state"
|
|
"github.com/gravitational/teleport/lib/auth/storage"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/backend/memory"
|
|
"github.com/gravitational/teleport/lib/cryptosuites"
|
|
"github.com/gravitational/teleport/lib/modules"
|
|
"github.com/gravitational/teleport/lib/modules/modulestest"
|
|
"github.com/gravitational/teleport/lib/observability/tracing"
|
|
"github.com/gravitational/teleport/lib/service/servicecfg"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/services/local"
|
|
"github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule"
|
|
"github.com/gravitational/teleport/lib/sshca"
|
|
"github.com/gravitational/teleport/lib/sshutils"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
"github.com/gravitational/teleport/lib/utils/proxy"
|
|
"github.com/gravitational/teleport/lib/utils/set"
|
|
)
|
|
|
|
// TestReadIdentity makes parses identity from private key and certificate
|
|
// and checks that all parameters are valid
|
|
func TestReadIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
clock := clockwork.NewFakeClock()
|
|
a, err := testauthority.NewKeygen(modules.BuildOSS, clock.Now)
|
|
require.NoError(t, err)
|
|
|
|
priv, pub, err := a.GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
caSigner, err := ssh.ParsePrivateKey(priv)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := a.GenerateHostCert(sshca.HostCertificateRequest{
|
|
CASigner: caSigner,
|
|
PublicHostKey: pub,
|
|
HostID: "id1",
|
|
NodeName: "node-name",
|
|
TTL: 0,
|
|
Identity: sshca.Identity{
|
|
ClusterName: "example.com",
|
|
SystemRole: types.RoleNode,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
id, err := state.ReadSSHIdentityFromKeyPair(priv, cert)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "example.com", id.ClusterName)
|
|
require.Equal(t, state.IdentityID{HostUUID: "id1.example.com", Role: types.RoleNode}, id.ID)
|
|
require.Equal(t, cert, id.CertBytes)
|
|
require.Equal(t, priv, id.KeyBytes)
|
|
|
|
// test TTL by converting the generated cert to text -> back and making sure ExpireAfter is valid
|
|
ttl := 10 * time.Second
|
|
expiryDate := clock.Now().Add(ttl)
|
|
bytes, err := a.GenerateHostCert(sshca.HostCertificateRequest{
|
|
CASigner: caSigner,
|
|
PublicHostKey: pub,
|
|
HostID: "id1",
|
|
NodeName: "node-name",
|
|
TTL: ttl,
|
|
Identity: sshca.Identity{
|
|
ClusterName: "example.com",
|
|
SystemRole: types.RoleNode,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
copy, err := apisshutils.ParseCertificate(bytes)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(expiryDate.Unix()), copy.ValidBefore)
|
|
}
|
|
|
|
func TestBadIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
a, err := testauthority.NewKeygen(modules.BuildOSS, time.Now)
|
|
require.NoError(t, err)
|
|
|
|
priv, pub, err := a.GenerateKeyPair()
|
|
require.NoError(t, err)
|
|
caSigner, err := ssh.ParsePrivateKey(priv)
|
|
require.NoError(t, err)
|
|
|
|
// bad cert type
|
|
_, err = state.ReadSSHIdentityFromKeyPair(priv, pub)
|
|
require.ErrorAs(t, err, new(*trace.BadParameterError))
|
|
|
|
// missing authority domain
|
|
cert, err := a.GenerateHostCert(sshca.HostCertificateRequest{
|
|
CASigner: caSigner,
|
|
PublicHostKey: pub,
|
|
HostID: "id2",
|
|
NodeName: "",
|
|
TTL: 0,
|
|
Identity: sshca.Identity{
|
|
ClusterName: "",
|
|
SystemRole: types.RoleNode,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = state.ReadSSHIdentityFromKeyPair(priv, cert)
|
|
require.ErrorAs(t, err, new(*trace.BadParameterError))
|
|
|
|
// missing host uuid
|
|
cert, err = a.GenerateHostCert(sshca.HostCertificateRequest{
|
|
CASigner: caSigner,
|
|
PublicHostKey: pub,
|
|
HostID: "example.com",
|
|
NodeName: "",
|
|
TTL: 0,
|
|
Identity: sshca.Identity{
|
|
ClusterName: "",
|
|
SystemRole: types.RoleNode,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = state.ReadSSHIdentityFromKeyPair(priv, cert)
|
|
require.ErrorAs(t, err, new(*trace.BadParameterError))
|
|
|
|
// unrecognized role
|
|
cert, err = a.GenerateHostCert(sshca.HostCertificateRequest{
|
|
CASigner: caSigner,
|
|
PublicHostKey: pub,
|
|
HostID: "example.com",
|
|
NodeName: "",
|
|
TTL: 0,
|
|
Identity: sshca.Identity{
|
|
ClusterName: "id1",
|
|
SystemRole: "bad role",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = state.ReadSSHIdentityFromKeyPair(priv, cert)
|
|
require.ErrorAs(t, err, new(*trace.BadParameterError))
|
|
}
|
|
|
|
func TestSignatureAlgorithmSuite(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := t.Context()
|
|
|
|
suiteName := func(suite types.SignatureAlgorithmSuite) string {
|
|
suiteName, err := suite.MarshalText()
|
|
require.NoError(t, err)
|
|
return string(suiteName)
|
|
}
|
|
|
|
assertSuitesEqual := func(t *testing.T, expected, actual types.SignatureAlgorithmSuite) {
|
|
t.Helper()
|
|
assert.Equal(t, suiteName(expected), suiteName(actual))
|
|
}
|
|
|
|
setupInitConfig := func(t *testing.T, capOrigin string, fips, hsm, cloud bool) auth.InitConfig {
|
|
cfg := setupConfig(t)
|
|
cfg.FIPS = fips
|
|
if hsm {
|
|
cfg.KeyStoreConfig = HSMTestConfig(t)
|
|
}
|
|
cfg.AuthPreference.SetOrigin(capOrigin)
|
|
if capOrigin != types.OriginDefaults {
|
|
cfg.AuthPreference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED)
|
|
}
|
|
// Pre-generate all CAs to keep tests fast esp. with SoftHSM.
|
|
for _, caType := range types.CertAuthTypes {
|
|
ca, err := authcatest.NewCAWithConfig(authcatest.CAConfig{
|
|
Type: caType,
|
|
ClusterName: cfg.ClusterName.GetClusterName(),
|
|
Clock: cfg.Clock,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
cfg.BootstrapResources = append(cfg.BootstrapResources, ca)
|
|
}
|
|
|
|
cfg.Modules = &modulestest.Modules{
|
|
TestFeatures: modules.Features{
|
|
Cloud: cloud,
|
|
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
|
|
entitlements.HSM: {Enabled: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
fips bool
|
|
hsm bool
|
|
cloud bool
|
|
expectDefaultSuite types.SignatureAlgorithmSuite
|
|
expectUnallowedSuites []types.SignatureAlgorithmSuite
|
|
}{
|
|
"basic": {
|
|
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
|
|
},
|
|
"fips": {
|
|
fips: true,
|
|
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1,
|
|
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
|
|
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
|
|
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
|
|
},
|
|
},
|
|
"hsm": {
|
|
hsm: true,
|
|
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
|
|
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
|
|
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
|
|
},
|
|
},
|
|
"fips and hsm": {
|
|
fips: true,
|
|
hsm: true,
|
|
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1,
|
|
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
|
|
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
|
|
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
|
|
},
|
|
},
|
|
"cloud": {
|
|
cloud: true,
|
|
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
|
|
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
|
|
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test the behavior of auth server init. A default signature algorithm
|
|
// suite should never overwrite a persisted signature algorithm suite for an
|
|
// existing cluster, even if that was also a default.
|
|
t.Run("init", func(t *testing.T) {
|
|
for _, origin := range []string{types.OriginDefaults, types.OriginConfigFile} {
|
|
t.Run(origin, func(t *testing.T) {
|
|
for desc, tc := range testCases {
|
|
t.Run(desc, func(t *testing.T) {
|
|
// Assert that a fresh cluster with no signature_algorithm_suite
|
|
// configured gets the expected default suite, whether
|
|
// or not anything else in the cluster auth preference is set.
|
|
cfg := setupInitConfig(t, origin, tc.fips, tc.hsm, tc.cloud)
|
|
auth1, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { auth1.Close() })
|
|
authPref, err := auth1.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
|
|
assertSuitesEqual(t, tc.expectDefaultSuite, authPref.GetSignatureAlgorithmSuite())
|
|
|
|
// Start a second auth server with the same backend and
|
|
// config, assert that the default suite remains.
|
|
auth2, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { auth2.Close() })
|
|
authPref, err = auth2.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
|
|
assertSuitesEqual(t, tc.expectDefaultSuite, authPref.GetSignatureAlgorithmSuite())
|
|
|
|
// In the stored cluster_auth_preference, reset the
|
|
// signature_algorithm_suite to unspecified (still with
|
|
// the same origin) to mimic an older cluster with the old
|
|
// defaults, in the next step it will be "upgraded".
|
|
authPref.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED)
|
|
_, err = auth2.UpsertAuthPreference(ctx, authPref)
|
|
require.NoError(t, err)
|
|
authPref, err = auth2.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
// Sanity check it persisted.
|
|
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
|
|
assertSuitesEqual(t, types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED, authPref.GetSignatureAlgorithmSuite())
|
|
|
|
// Start a third brand new auth server sharing the same
|
|
// backend and config. The new auth starting up should
|
|
// apply the new default auth preference and persist it
|
|
// to the backend, but it should not modify the existing
|
|
// signature algorithm suite even though it's
|
|
// unspecified. This is meant to test that a v16 auth
|
|
// server upgraded to v17 will still have an unspecified
|
|
// signature algorithm suite and won't get a new one
|
|
// until explicitly opting in.
|
|
auth3, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { auth3.Close() })
|
|
authPref, err = auth3.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
|
|
assertSuitesEqual(t, types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED, authPref.GetSignatureAlgorithmSuite())
|
|
|
|
// Assert that the selected algorithm is RSA2048 when the suite is
|
|
// unspecified.
|
|
alg, err := cryptosuites.AlgorithmForKey(ctx,
|
|
cryptosuites.GetCurrentSuiteFromAuthPreference(auth3),
|
|
cryptosuites.UserTLS)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, cryptosuites.RSA2048.String(), alg.String())
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// Test that the auth preference cannot be upserted with a signature
|
|
// algorithm suite incompatible with the cluster FIPS and HSM settings.
|
|
t.Run("upsert", func(t *testing.T) {
|
|
for desc, tc := range testCases {
|
|
t.Run(desc, func(t *testing.T) {
|
|
cfg := authtest.AuthServerConfig{
|
|
Dir: t.TempDir(),
|
|
FIPS: tc.fips,
|
|
AuthPreferenceSpec: &types.AuthPreferenceSpecV2{
|
|
// Cloud requires second factor enabled.
|
|
SecondFactor: constants.SecondFactorOn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "teleport.example.com",
|
|
},
|
|
},
|
|
Modules: &modulestest.Modules{
|
|
TestFeatures: modules.Features{
|
|
Cloud: tc.cloud,
|
|
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
|
|
entitlements.HSM: {Enabled: true},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if tc.hsm {
|
|
cfg.KeystoreConfig = HSMTestConfig(t)
|
|
}
|
|
testAuthServer, err := authtest.NewAuthServer(cfg)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, testAuthServer.Close()) })
|
|
|
|
tlsServer, err := testAuthServer.NewTestTLSServer()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, tlsServer.Close()) })
|
|
|
|
clt, err := tlsServer.NewClient(authtest.TestAdmin())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, clt.Close()) })
|
|
|
|
for _, suiteValue := range types.SignatureAlgorithmSuite_value {
|
|
suite := types.SignatureAlgorithmSuite(suiteValue)
|
|
t.Run(suiteName(suite), func(t *testing.T) {
|
|
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
SignatureAlgorithmSuite: suite,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = clt.UpsertAuthPreference(ctx, authPref)
|
|
if slices.Contains(tc.expectUnallowedSuites, suite) {
|
|
var badParameterErr *trace.BadParameterError
|
|
assert.ErrorAs(t, err, &badParameterErr)
|
|
return
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Reset should go back to the default suite.
|
|
err = clt.ResetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
authPref, err = clt.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, types.OriginDefaults, authPref.GetMetadata().Labels[types.OriginLabel])
|
|
assertSuitesEqual(t, tc.expectDefaultSuite, authPref.GetSignatureAlgorithmSuite())
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func HSMTestConfig(t *testing.T) servicecfg.KeystoreConfig {
|
|
if cfg, ok := yubiHSMTestConfig(); ok {
|
|
t.Log("Running test with YubiHSM")
|
|
return cfg
|
|
}
|
|
if cfg, ok := cloudHSMTestConfig(); ok {
|
|
t.Log("Running test with AWS CloudHSM")
|
|
return cfg
|
|
}
|
|
if cfg, ok := awsKMSTestConfig(); ok {
|
|
t.Log("Running test with AWS KMS")
|
|
return cfg
|
|
}
|
|
if cfg, ok := gcpKMSTestConfig(); ok {
|
|
t.Log("Running test with GCP KMS")
|
|
return cfg
|
|
}
|
|
if cfg, ok := softHSMTestConfig(t); ok {
|
|
t.Log("Running test with SoftHSM")
|
|
return cfg
|
|
}
|
|
t.Skip("No HSM available for test")
|
|
return servicecfg.KeystoreConfig{}
|
|
}
|
|
|
|
func yubiHSMTestConfig() (servicecfg.KeystoreConfig, bool) {
|
|
yubiHSMPath := os.Getenv("TELEPORT_TEST_YUBIHSM_PKCS11_PATH")
|
|
yubiHSMPin := os.Getenv("TELEPORT_TEST_YUBIHSM_PIN")
|
|
if yubiHSMPath == "" || yubiHSMPin == "" {
|
|
return servicecfg.KeystoreConfig{}, false
|
|
}
|
|
slotNumber := 0
|
|
return servicecfg.KeystoreConfig{
|
|
PKCS11: servicecfg.PKCS11Config{
|
|
Path: yubiHSMPath,
|
|
SlotNumber: &slotNumber,
|
|
PIN: yubiHSMPin,
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func cloudHSMTestConfig() (servicecfg.KeystoreConfig, bool) {
|
|
cloudHSMPin := os.Getenv("TELEPORT_TEST_CLOUDHSM_PIN")
|
|
if cloudHSMPin == "" {
|
|
return servicecfg.KeystoreConfig{}, false
|
|
}
|
|
return servicecfg.KeystoreConfig{
|
|
PKCS11: servicecfg.PKCS11Config{
|
|
Path: "/opt/cloudhsm/lib/libcloudhsm_pkcs11.so",
|
|
TokenLabel: "cavium",
|
|
PIN: cloudHSMPin,
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func awsKMSTestConfig() (servicecfg.KeystoreConfig, bool) {
|
|
awsKMSAccount := os.Getenv("TELEPORT_TEST_AWS_KMS_ACCOUNT")
|
|
awsKMSRegion := os.Getenv("TELEPORT_TEST_AWS_KMS_REGION")
|
|
if awsKMSAccount == "" || awsKMSRegion == "" {
|
|
return servicecfg.KeystoreConfig{}, false
|
|
}
|
|
return servicecfg.KeystoreConfig{
|
|
AWSKMS: &servicecfg.AWSKMSConfig{
|
|
AWSAccount: awsKMSAccount,
|
|
AWSRegion: awsKMSRegion,
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func gcpKMSTestConfig() (servicecfg.KeystoreConfig, bool) {
|
|
gcpKeyring := os.Getenv("TELEPORT_TEST_GCP_KMS_KEYRING")
|
|
if gcpKeyring == "" {
|
|
return servicecfg.KeystoreConfig{}, false
|
|
}
|
|
return servicecfg.KeystoreConfig{
|
|
GCPKMS: servicecfg.GCPKMSConfig{
|
|
KeyRing: gcpKeyring,
|
|
ProtectionLevel: "SOFTWARE",
|
|
},
|
|
}, true
|
|
}
|
|
|
|
var (
|
|
cachedSoftHSMConfig *servicecfg.KeystoreConfig
|
|
cachedSoftHSMConfigMutex sync.Mutex
|
|
)
|
|
|
|
// softHSMTestConfig is for use in tests only and creates a test SOFTHSM2 token.
|
|
// This should be used for all tests which need to use SoftHSM because the
|
|
// library can only be initialized once and SOFTHSM2_PATH and SOFTHSM2_CONF
|
|
// cannot be changed. New tokens added after the library has been initialized
|
|
// will not be found by the library.
|
|
//
|
|
// A new token will be used for each `go test` invocation, but it's difficult
|
|
// to create a separate token for each test because because new tokens
|
|
// added after the library has been initialized will not be found by the
|
|
// library. It's also difficult to clean up the token because tests for all
|
|
// packages are run in parallel there is not a good time to safely
|
|
// delete the token or the entire token directory. Each test should clean up
|
|
// all keys that it creates because SoftHSM2 gets really slow when there are
|
|
// many keys for a given token.
|
|
func softHSMTestConfig(t *testing.T) (servicecfg.KeystoreConfig, bool) {
|
|
path := os.Getenv("SOFTHSM2_PATH")
|
|
if path == "" {
|
|
return servicecfg.KeystoreConfig{}, false
|
|
}
|
|
|
|
cachedSoftHSMConfigMutex.Lock()
|
|
defer cachedSoftHSMConfigMutex.Unlock()
|
|
|
|
if cachedSoftHSMConfig != nil {
|
|
return *cachedSoftHSMConfig, true
|
|
}
|
|
|
|
if os.Getenv("SOFTHSM2_CONF") == "" {
|
|
// create tokendir
|
|
tokenDir, err := os.MkdirTemp("", "tokens")
|
|
require.NoError(t, err)
|
|
|
|
// create config file
|
|
configFile, err := os.CreateTemp("", "softhsm2.conf")
|
|
require.NoError(t, err)
|
|
|
|
// write config file
|
|
_, err = fmt.Fprintf(configFile, "directories.tokendir = %s\nobjectstore.backend = file\nlog.level = DEBUG\n", tokenDir)
|
|
require.NoError(t, err)
|
|
require.NoError(t, configFile.Close())
|
|
|
|
// set env
|
|
os.Setenv("SOFTHSM2_CONF", configFile.Name())
|
|
}
|
|
|
|
// create test token (max length is 32 chars)
|
|
tokenLabel := strings.ReplaceAll(uuid.NewString(), "-", "")
|
|
cmd := exec.Command("softhsm2-util", "--init-token", "--free", "--label", tokenLabel, "--so-pin", "password", "--pin", "password")
|
|
t.Logf("Running command: %q", cmd)
|
|
if err := cmd.Run(); err != nil {
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
require.NoError(t, exitErr, "error creating test softhsm token: %s", string(exitErr.Stderr))
|
|
}
|
|
require.NoError(t, err, "error attempting to run softhsm2-util")
|
|
}
|
|
|
|
cachedSoftHSMConfig = &servicecfg.KeystoreConfig{
|
|
PKCS11: servicecfg.PKCS11Config{
|
|
Path: path,
|
|
TokenLabel: tokenLabel,
|
|
PIN: "password",
|
|
},
|
|
}
|
|
return *cachedSoftHSMConfig, true
|
|
}
|
|
|
|
type testDynamicallyConfigurableParams struct {
|
|
withDefaults, withConfigFile, withAnotherConfigFile func(*testing.T, *auth.InitConfig) types.ResourceWithOrigin
|
|
setDynamic func(*testing.T, *auth.Server)
|
|
getStored func(*testing.T, *auth.Server) types.ResourceWithOrigin
|
|
}
|
|
|
|
func testDynamicallyConfigurable(t *testing.T, p testDynamicallyConfigurableParams) {
|
|
initAuthServer := func(t *testing.T, conf auth.InitConfig) *auth.Server {
|
|
authServer, err := auth.Init(context.Background(), conf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { authServer.Close() })
|
|
return authServer
|
|
}
|
|
|
|
t.Run("start with config file, reinit with defaults", func(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
|
|
// Simulate a server with a config-file resource.
|
|
configFileRes := p.withConfigFile(t, &conf)
|
|
authServer := initAuthServer(t, conf)
|
|
|
|
stored := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginConfigFile, stored.Origin())
|
|
require.Empty(t, resourceDiff(configFileRes, stored))
|
|
|
|
// Reinitialize with the default resource.
|
|
defaultRes := p.withDefaults(t, &conf)
|
|
authServer = initAuthServer(t, conf)
|
|
|
|
// Verify the stored resource is now labeled as originating from defaults.
|
|
stored = p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginDefaults, stored.Origin())
|
|
require.Empty(t, resourceDiff(defaultRes, stored))
|
|
})
|
|
|
|
t.Run("start with dynamic, reinit with defaults", func(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
|
|
// Simulate a server with dynamic configuration.
|
|
authServer := initAuthServer(t, conf)
|
|
p.setDynamic(t, authServer)
|
|
|
|
dynamic := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginDynamic, dynamic.Origin())
|
|
|
|
// Attempt to reinitialize with the default resource should be a no-op.
|
|
p.withDefaults(t, &conf)
|
|
authServer = initAuthServer(t, conf)
|
|
|
|
// Verify the stored resource remains unchanged.
|
|
stored := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginDynamic, stored.Origin())
|
|
require.Empty(t, resourceDiff(dynamic, stored))
|
|
})
|
|
|
|
t.Run("start with dynamic, reinit with config file", func(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
|
|
// Simulate a server with dynamic configuration.
|
|
authServer := initAuthServer(t, conf)
|
|
p.setDynamic(t, authServer)
|
|
|
|
dynamic := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginDynamic, dynamic.Origin())
|
|
|
|
// Reinitialize with a config-file resource.
|
|
configFileRes := p.withConfigFile(t, &conf)
|
|
authServer = initAuthServer(t, conf)
|
|
|
|
// Verify the stored resource is updated.
|
|
stored := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginConfigFile, stored.Origin())
|
|
require.Empty(t, resourceDiff(configFileRes, stored))
|
|
})
|
|
|
|
t.Run("start with defaults, reinit with config file", func(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
|
|
// Simulate a server with the default resource.
|
|
defaultRes := p.withDefaults(t, &conf)
|
|
authServer := initAuthServer(t, conf)
|
|
|
|
stored := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginDefaults, stored.Origin())
|
|
require.Empty(t, resourceDiff(defaultRes, stored))
|
|
|
|
// Reinitialize with a config-file resource.
|
|
configFileRes := p.withConfigFile(t, &conf)
|
|
authServer = initAuthServer(t, conf)
|
|
|
|
// Verify the stored resource is updated.
|
|
stored = p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginConfigFile, stored.Origin())
|
|
require.Empty(t, resourceDiff(configFileRes, stored))
|
|
})
|
|
|
|
t.Run("start with config file, reinit with another config file", func(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
|
|
// Simulate a server with a config-file resource.
|
|
configFileRes := p.withConfigFile(t, &conf)
|
|
authServer := initAuthServer(t, conf)
|
|
|
|
stored := p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginConfigFile, stored.Origin())
|
|
require.Empty(t, resourceDiff(configFileRes, stored))
|
|
|
|
// Reinitialize with another config-file resource.
|
|
anotherConfigFileRes := p.withAnotherConfigFile(t, &conf)
|
|
authServer = initAuthServer(t, conf)
|
|
|
|
// Verify the stored resource is updated.
|
|
stored = p.getStored(t, authServer)
|
|
require.Equal(t, types.OriginConfigFile, stored.Origin())
|
|
require.Empty(t, resourceDiff(anotherConfigFileRes, stored))
|
|
})
|
|
}
|
|
|
|
func TestAuthPreference(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
testDynamicallyConfigurable(t, testDynamicallyConfigurableParams{
|
|
withDefaults: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
conf.AuthPreference = types.DefaultAuthPreference()
|
|
return conf.AuthPreference
|
|
},
|
|
withConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
fromConfigFile, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
|
|
Type: constants.OIDC,
|
|
SignatureAlgorithmSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
|
|
})
|
|
require.NoError(t, err)
|
|
conf.AuthPreference = fromConfigFile
|
|
return conf.AuthPreference
|
|
},
|
|
withAnotherConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
conf.AuthPreference = newWebauthnAuthPreferenceConfigFromFile(t)
|
|
conf.AuthPreference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1)
|
|
return conf.AuthPreference
|
|
},
|
|
setDynamic: func(t *testing.T, authServer *auth.Server) {
|
|
dynamically, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
SecondFactor: constants.SecondFactorOff,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = authServer.UpsertAuthPreference(ctx, dynamically)
|
|
require.NoError(t, err)
|
|
},
|
|
getStored: func(t *testing.T, authServer *auth.Server) types.ResourceWithOrigin {
|
|
authPref, err := authServer.GetAuthPreference(ctx)
|
|
require.NoError(t, err)
|
|
return authPref
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestVnetConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
conf := setupConfig(t)
|
|
authServer, err := auth.Init(context.Background(), conf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { authServer.Close() })
|
|
|
|
actualVnetConfig, err := authServer.GetVnetConfig(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
defaultVnetConfig, err := vnet.DefaultVnetConfig()
|
|
require.NoError(t, err)
|
|
require.Empty(t, cmp.Diff(defaultVnetConfig.GetSpec(), actualVnetConfig.GetSpec(), protocmp.Transform()))
|
|
}
|
|
|
|
func TestAuthPreferenceSecondFactorOnly(t *testing.T) {
|
|
modules.SetInsecureTestMode(false)
|
|
defer modules.SetInsecureTestMode(true)
|
|
ctx := context.Background()
|
|
|
|
conf := setupConfig(t)
|
|
authPref, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
|
|
SecondFactor: constants.SecondFactorOff,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
conf.AuthPreference = authPref
|
|
_, err = auth.Init(ctx, conf)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestClusterNetworkingConfig(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
testDynamicallyConfigurable(t, testDynamicallyConfigurableParams{
|
|
withDefaults: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
conf.ClusterNetworkingConfig = types.DefaultClusterNetworkingConfig()
|
|
return conf.ClusterNetworkingConfig
|
|
},
|
|
withConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
fromConfigFile, err := types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
|
|
ClientIdleTimeout: types.Duration(7 * time.Minute),
|
|
})
|
|
require.NoError(t, err)
|
|
conf.ClusterNetworkingConfig = fromConfigFile
|
|
return conf.ClusterNetworkingConfig
|
|
},
|
|
withAnotherConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
anotherFromConfigFile, err := types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
|
|
ClientIdleTimeout: types.Duration(10 * time.Minute),
|
|
KeepAliveInterval: types.Duration(3 * time.Minute),
|
|
})
|
|
require.NoError(t, err)
|
|
conf.ClusterNetworkingConfig = anotherFromConfigFile
|
|
return conf.ClusterNetworkingConfig
|
|
},
|
|
setDynamic: func(t *testing.T, authServer *auth.Server) {
|
|
dynamically, err := types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
|
|
KeepAliveInterval: types.Duration(4 * time.Minute),
|
|
})
|
|
require.NoError(t, err)
|
|
dynamically.SetOrigin(types.OriginDynamic)
|
|
_, err = authServer.UpsertClusterNetworkingConfig(ctx, dynamically)
|
|
require.NoError(t, err)
|
|
},
|
|
getStored: func(t *testing.T, authServer *auth.Server) types.ResourceWithOrigin {
|
|
authPref, err := authServer.GetClusterNetworkingConfig(ctx)
|
|
require.NoError(t, err)
|
|
return authPref
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSessionRecordingConfig(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
testDynamicallyConfigurable(t, testDynamicallyConfigurableParams{
|
|
withDefaults: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
conf.SessionRecordingConfig = types.DefaultSessionRecordingConfig()
|
|
return conf.SessionRecordingConfig
|
|
},
|
|
withConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
fromConfigFile, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
|
|
Mode: types.RecordOff,
|
|
})
|
|
require.NoError(t, err)
|
|
conf.SessionRecordingConfig = fromConfigFile
|
|
return conf.SessionRecordingConfig
|
|
},
|
|
withAnotherConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
|
|
anotherFromConfigFile, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
|
|
Mode: types.RecordAtProxySync,
|
|
})
|
|
require.NoError(t, err)
|
|
conf.SessionRecordingConfig = anotherFromConfigFile
|
|
return conf.SessionRecordingConfig
|
|
},
|
|
setDynamic: func(t *testing.T, authServer *auth.Server) {
|
|
dynamically, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
|
|
Mode: types.RecordAtNodeSync,
|
|
})
|
|
require.NoError(t, err)
|
|
dynamically.SetOrigin(types.OriginDynamic)
|
|
_, err = authServer.UpsertSessionRecordingConfig(ctx, dynamically)
|
|
require.NoError(t, err)
|
|
},
|
|
getStored: func(t *testing.T, authServer *auth.Server) types.ResourceWithOrigin {
|
|
authPref, err := authServer.GetSessionRecordingConfig(ctx)
|
|
require.NoError(t, err)
|
|
return authPref
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestClusterID(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
ctx := context.Background()
|
|
authServer, err := auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
defer authServer.Close()
|
|
|
|
cc, err := authServer.GetClusterName(ctx)
|
|
require.NoError(t, err)
|
|
clusterID := cc.GetClusterID()
|
|
require.NotEmpty(t, clusterID)
|
|
|
|
// do it again and make sure cluster ID hasn't changed
|
|
authServer, err = auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
defer authServer.Close()
|
|
|
|
cc, err = authServer.GetClusterName(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, clusterID, cc.GetClusterID())
|
|
}
|
|
|
|
// TestClusterName ensures that a cluster can not be renamed.
|
|
func TestClusterName(t *testing.T) {
|
|
t.Parallel()
|
|
conf := setupConfig(t)
|
|
ctx := context.Background()
|
|
authServer, err := auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
defer authServer.Close()
|
|
|
|
// Start the auth server with a different cluster name. The auth server
|
|
// should start, but with the original name.
|
|
newConfig := conf
|
|
newConfig.ClusterName, err = services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
|
|
ClusterName: "dev.localhost",
|
|
})
|
|
require.NoError(t, err)
|
|
authServer, err = auth.Init(context.Background(), newConfig)
|
|
require.NoError(t, err)
|
|
defer authServer.Close()
|
|
|
|
cn, err := authServer.GetClusterName(ctx)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, newConfig.ClusterName.GetClusterName(), cn.GetClusterName())
|
|
require.Equal(t, conf.ClusterName.GetClusterName(), cn.GetClusterName())
|
|
}
|
|
|
|
type failingTrustInternal struct {
|
|
services.TrustInternal
|
|
}
|
|
|
|
func (t *failingTrustInternal) CreateCertAuthority(ctx context.Context, ca types.CertAuthority) error {
|
|
return trace.Errorf("error")
|
|
}
|
|
|
|
// TestInitCertFailureRecovery ensures the auth server is able to recover from
|
|
// a failure in the cert creation process.
|
|
func TestInitCertFailureRecovery(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
cap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
|
Type: constants.SAML,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
conf := setupConfig(t)
|
|
|
|
// BootstrapResources have lead to an unrecoverable state in the past.
|
|
// See https://github.com/gravitational/teleport/pull/49638.
|
|
conf.BootstrapResources = []types.Resource{cap}
|
|
_, err = auth.Init(ctx, conf, func(s *auth.Server) error {
|
|
s.TrustInternal = &failingTrustInternal{
|
|
TrustInternal: s.TrustInternal,
|
|
}
|
|
return nil
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, err = auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestPresets tests behavior of presets
|
|
func TestPresets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
|
|
presetRoleNames := []string{
|
|
teleport.PresetEditorRoleName,
|
|
teleport.PresetAccessRoleName,
|
|
teleport.PresetAuditorRoleName,
|
|
teleport.PresetTerraformProviderRoleName,
|
|
teleport.PresetWildcardWorkloadIdentityIssuerRoleName,
|
|
teleport.PresetAccessPluginRoleName,
|
|
teleport.PresetAccessPluginWithReviewRoleName,
|
|
teleport.PresetListAccessRequestResourcesRoleName,
|
|
teleport.PresetMCPUserRoleName,
|
|
}
|
|
|
|
t.Run("EmptyCluster", func(t *testing.T) {
|
|
testModules := modulestest.OSSModules()
|
|
as := newTestAuthServer(ctx, t, testModules)
|
|
|
|
err := auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
|
|
// Second call should not fail
|
|
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
|
|
// Presets were created
|
|
for _, role := range presetRoleNames {
|
|
_, err := as.GetRole(ctx, role)
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
// Makes sure that existing role with the same name is not modified
|
|
t.Run("ExistingRole", func(t *testing.T) {
|
|
testModules := modulestest.OSSModules()
|
|
|
|
as := newTestAuthServer(ctx, t, testModules)
|
|
|
|
access := services.NewPresetEditorRole()
|
|
access.SetLogins(types.Allow, []string{"root"})
|
|
access, err := as.CreateRole(ctx, access)
|
|
require.NoError(t, err)
|
|
|
|
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
|
|
// Presets were created
|
|
for _, role := range presetRoleNames {
|
|
_, err := as.GetRole(ctx, role)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
out, err := as.GetRole(ctx, access.GetName())
|
|
require.NoError(t, err)
|
|
require.Equal(t, access.GetLogins(types.Allow), out.GetLogins(types.Allow))
|
|
})
|
|
|
|
// If a default allow condition is not present, ensure it gets added.
|
|
t.Run("AddDefaultAllowConditions", func(t *testing.T) {
|
|
testModules := modulestest.OSSModules()
|
|
|
|
as := newTestAuthServer(ctx, t, testModules)
|
|
|
|
editorRole := services.NewPresetEditorRole()
|
|
rules := editorRole.GetRules(types.Allow)
|
|
|
|
// Create a new set of rules based on the Editor Role, excluding the ConnectionDiagnostic.
|
|
// ConnectionDiagnostic is part of the default allow rules
|
|
var outdatedRules []types.Rule
|
|
for _, r := range rules {
|
|
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
|
|
continue
|
|
}
|
|
outdatedRules = append(outdatedRules, r)
|
|
}
|
|
editorRole.SetRules(types.Allow, outdatedRules)
|
|
editorRole, err := as.CreateRole(ctx, editorRole)
|
|
require.NoError(t, err)
|
|
|
|
// Set up an old Access Role.
|
|
// Remove the new DatabaseServiceLabels default
|
|
accessRole := services.NewPresetAccessRole()
|
|
accessRole.SetDatabaseServiceLabels(types.Allow, types.Labels{})
|
|
accessRole, err = as.CreateRole(ctx, accessRole)
|
|
require.NoError(t, err)
|
|
|
|
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
|
|
outEditor, err := as.GetRole(ctx, editorRole.GetName())
|
|
require.NoError(t, err)
|
|
|
|
allowRules := outEditor.GetRules(types.Allow)
|
|
require.Condition(t, func() (success bool) {
|
|
for _, r := range allowRules {
|
|
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, "missing default rule")
|
|
|
|
outAccess, err := as.GetRole(ctx, accessRole.GetName())
|
|
require.NoError(t, err)
|
|
allowedDatabaseServiceLabels := outAccess.GetDatabaseServiceLabels(types.Allow)
|
|
require.Equal(t, types.Labels{types.Wildcard: []string{types.Wildcard}}, allowedDatabaseServiceLabels, "missing default DatabaseServiceLabels")
|
|
})
|
|
|
|
// Don't set a default allow rule if the resource is present in the role.
|
|
// Either as part of allowing or denying rules.
|
|
t.Run("DefaultAllowRulesNotAppliedIfExplicitlyDefined", func(t *testing.T) {
|
|
testModules := modulestest.OSSModules()
|
|
as := newTestAuthServer(ctx, t, testModules)
|
|
|
|
// Set up a changed Editor Role
|
|
editorRole := services.NewPresetEditorRole()
|
|
allowRules := editorRole.GetRules(types.Allow)
|
|
|
|
// Create a new set of rules based on the Editor Role,
|
|
// setting a deny rule for a default allow rule
|
|
var outdateAllowRules []types.Rule
|
|
for _, r := range allowRules {
|
|
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
|
|
continue
|
|
}
|
|
outdateAllowRules = append(outdateAllowRules, r)
|
|
}
|
|
editorRole.SetRules(types.Allow, outdateAllowRules)
|
|
|
|
// Explicitly deny Create to ConnectionDiagnostic
|
|
denyRules := editorRole.GetRules(types.Deny)
|
|
denyConnectionDiagnosticRule := types.NewRule(types.KindConnectionDiagnostic, []string{types.VerbCreate})
|
|
denyRules = append(denyRules, denyConnectionDiagnosticRule)
|
|
editorRole.SetRules(types.Deny, denyRules)
|
|
|
|
editorRole, err := as.CreateRole(ctx, editorRole)
|
|
require.NoError(t, err)
|
|
|
|
// Set up a changed Access Role
|
|
accessRole := services.NewPresetAccessRole()
|
|
// Remove a default allow label as well.
|
|
accessRole.SetDatabaseServiceLabels(types.Allow, types.Labels{})
|
|
// Explicitly deny DatabaseServiceLabels
|
|
accessRole.SetDatabaseServiceLabels(types.Deny, types.Labels{types.Wildcard: []string{types.Wildcard}})
|
|
|
|
accessRole, err = as.CreateRole(ctx, accessRole)
|
|
require.NoError(t, err)
|
|
|
|
// Apply defaults.
|
|
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
|
|
outEditor, err := as.GetRole(ctx, editorRole.GetName())
|
|
require.NoError(t, err)
|
|
|
|
allowRules = outEditor.GetRules(types.Allow)
|
|
require.Condition(t, func() (success bool) {
|
|
for _, r := range allowRules {
|
|
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}, "missing default rule")
|
|
|
|
outAccess, err := as.GetRole(ctx, accessRole.GetName())
|
|
require.NoError(t, err)
|
|
allowedDatabaseServiceLabels := outAccess.GetDatabaseServiceLabels(types.Allow)
|
|
require.Nil(t, allowedDatabaseServiceLabels, "does not set Allowed DatabaseService Labels")
|
|
deniedDatabaseServiceLabels := outAccess.GetDatabaseServiceLabels(types.Deny)
|
|
require.Equal(t, types.Labels{types.Wildcard: []string{types.Wildcard}}, deniedDatabaseServiceLabels, "keeps the deny label for DatabaseService")
|
|
})
|
|
|
|
upsertRoleTest := func(t *testing.T, buildType string, expectedPresetRoles []string, expectedSystemRoles []string) {
|
|
// test state
|
|
ctx := context.Background()
|
|
// mu protects created resource maps
|
|
var mu sync.Mutex
|
|
createdSystemRoles := make(map[string]types.Role)
|
|
createdPresets := make(map[string]types.Role)
|
|
|
|
//
|
|
// Test #1 - populating an empty cluster
|
|
//
|
|
roleManager := newMockRoleManager(t)
|
|
|
|
// EXPECT that non-system resources will be created once
|
|
// and once only.
|
|
roleManager.
|
|
On("CreateRole", mock.Anything, mock.Anything).
|
|
Run(func(args mock.Arguments) {
|
|
r := args[1].(types.Role)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
require.Contains(t, expectedPresetRoles, r.GetName())
|
|
require.NotContains(t, createdPresets, r.GetName())
|
|
require.False(t, types.IsSystemResource(r) && r.GetName() != teleport.SystemOktaRequesterRoleName)
|
|
createdPresets[r.GetName()] = r
|
|
}).
|
|
Return(func(_ context.Context, r types.Role) (types.Role, error) {
|
|
return r, nil
|
|
})
|
|
|
|
// EXPECT that any (and ONLY) system resources will be upserted
|
|
roleManager.
|
|
On("UpsertRole", mock.Anything, mock.Anything).
|
|
Run(func(args mock.Arguments) {
|
|
r := args[1].(types.Role)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
require.True(t, types.IsSystemResource(r))
|
|
require.Contains(t, expectedSystemRoles, r.GetName())
|
|
require.NotContains(t, slices.Collect(maps.Keys(createdSystemRoles)), r.GetName())
|
|
createdSystemRoles[r.GetName()] = r
|
|
}).
|
|
Maybe().
|
|
Return(func(_ context.Context, r types.Role) (types.Role, error) {
|
|
return r, nil
|
|
})
|
|
|
|
err := auth.CreatePresetRoles(ctx, buildType, roleManager)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, slices.Collect(maps.Keys(createdPresets)), expectedPresetRoles)
|
|
require.ElementsMatch(t, slices.Collect(maps.Keys(createdSystemRoles)), expectedSystemRoles)
|
|
roleManager.AssertExpectations(t)
|
|
|
|
//
|
|
// Test #2 - populating an already-populated cluster
|
|
//
|
|
roleManager = newMockRoleManager(t)
|
|
|
|
// EXPECT that createPresets will try to create all expected
|
|
// non-system roles
|
|
roleManager.
|
|
On("CreateRole", mock.Anything, mock.Anything).
|
|
Run(func(args mock.Arguments) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
require.Contains(t, createdPresets, args[1].(types.Role).GetName())
|
|
}).
|
|
Return(nil, trace.AlreadyExists("dupe"))
|
|
|
|
// EXPECT that any (and ONLY) expected system roles will be
|
|
// automatically upserted
|
|
roleManager.
|
|
On("UpsertRole", mock.Anything, mock.Anything).
|
|
Run(requireSystemResource(t, 1)).
|
|
Maybe().
|
|
Return(func(_ context.Context, r types.Role) (types.Role, error) {
|
|
return r, nil
|
|
})
|
|
|
|
// EXPECT that all of the roles created in the previous step (and ONLY the
|
|
// roles created in the previous step will be queried.
|
|
for name, role := range createdPresets {
|
|
roleManager.
|
|
On("GetRole", mock.Anything, name).
|
|
Return(role, nil)
|
|
}
|
|
|
|
err = auth.CreatePresetRoles(ctx, buildType, roleManager)
|
|
require.NoError(t, err)
|
|
roleManager.AssertExpectations(t)
|
|
|
|
//
|
|
// Test #3 - populating an already-populated cluster with updated presets
|
|
//
|
|
roleManager = newMockRoleManager(t)
|
|
|
|
// Removing a specific resource which is part of the Default Allow Rules
|
|
// should trigger an UpsertRole call
|
|
editorRole := createdPresets[teleport.PresetEditorRoleName]
|
|
var allowRulesWithoutConnectionDiag []types.Rule
|
|
|
|
for _, r := range editorRole.GetRules(types.Allow) {
|
|
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
|
|
continue
|
|
}
|
|
allowRulesWithoutConnectionDiag = append(allowRulesWithoutConnectionDiag, r)
|
|
}
|
|
editorRole.SetRules(types.Allow, allowRulesWithoutConnectionDiag)
|
|
|
|
// EXPECT that createPresets will try to create all expected
|
|
// non-system roles
|
|
remainingPresets := set.New(expectedPresetRoles...)
|
|
roleManager.
|
|
On("CreateRole", mock.Anything, mock.Anything).
|
|
Run(func(args mock.Arguments) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
r := args[1].(types.Role)
|
|
require.Contains(t, createdPresets, r.GetName())
|
|
delete(remainingPresets, r.GetName())
|
|
}).
|
|
Return(nil, trace.AlreadyExists("dupe"))
|
|
|
|
// EXPECT that all of the roles created in the first step (and ONLY the
|
|
// roles created in the first step will be queried.
|
|
for name, role := range createdPresets {
|
|
roleManager.
|
|
On("GetRole", mock.Anything, name).
|
|
Return(role, nil)
|
|
}
|
|
|
|
// EXPECT that any system roles will be automatically upserted
|
|
// AND our modified editor resource will be updated using an upsert
|
|
roleManager.
|
|
On("UpsertRole", mock.Anything, mock.Anything).
|
|
Return(func(_ context.Context, r types.Role) (types.Role, error) {
|
|
if types.IsSystemResource(r) {
|
|
require.Contains(t, expectedSystemRoles, r.GetName())
|
|
return r, nil
|
|
}
|
|
require.Equal(t, teleport.PresetEditorRoleName, r.GetName())
|
|
return r, nil
|
|
})
|
|
|
|
err = auth.CreatePresetRoles(ctx, buildType, roleManager)
|
|
require.NoError(t, err)
|
|
require.Empty(t, remainingPresets)
|
|
roleManager.AssertExpectations(t)
|
|
}
|
|
|
|
t.Run("Does not upsert roles if nothing changes", func(t *testing.T) {
|
|
upsertRoleTest(t, modules.BuildOSS, presetRoleNames, nil)
|
|
})
|
|
|
|
t.Run("Enterprise", func(t *testing.T) {
|
|
testModules := modulestest.EnterpriseModules()
|
|
|
|
enterprisePresetRoleNames := append([]string{
|
|
teleport.PresetGroupAccessRoleName,
|
|
teleport.PresetRequesterRoleName,
|
|
teleport.PresetReviewerRoleName,
|
|
teleport.PresetDeviceAdminRoleName,
|
|
teleport.PresetDeviceEnrollRoleName,
|
|
teleport.PresetRequireTrustedDeviceRoleName,
|
|
teleport.SystemOktaRequesterRoleName, // This is treated as a preset
|
|
teleport.PresetBeamUserRoleName,
|
|
teleport.PresetBeamAdminRoleName,
|
|
}, presetRoleNames...)
|
|
|
|
enterpriseSystemRoleNames := []string{
|
|
teleport.SystemAutomaticAccessApprovalRoleName,
|
|
teleport.SystemOktaAccessRoleName,
|
|
teleport.SystemIdentityCenterAccessRoleName,
|
|
teleport.SystemBeamRoleName,
|
|
}
|
|
|
|
enterpriseUsers := []types.User{
|
|
services.NewSystemAutomaticAccessBotUser(testModules.BuildType()),
|
|
}
|
|
|
|
t.Run("EmptyCluster", func(t *testing.T) {
|
|
as := newTestAuthServer(ctx, t, testModules)
|
|
|
|
// Run multiple times to simulate starting auth on an
|
|
// existing cluster and asserting that everything still
|
|
// returns success
|
|
for range 2 {
|
|
err := auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
|
|
err = auth.CreatePresetUsers(ctx, testModules.BuildType(), as)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Preset Roles were created
|
|
for _, role := range append(enterprisePresetRoleNames, enterpriseSystemRoleNames...) {
|
|
_, err := as.GetRole(ctx, role)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Preset Users were created
|
|
for _, user := range enterpriseUsers {
|
|
_, err := as.GetUser(ctx, user.GetName(), false)
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Does not upsert roles if nothing changes", func(t *testing.T) {
|
|
upsertRoleTest(t, testModules.BuildType(), enterprisePresetRoleNames, enterpriseSystemRoleNames)
|
|
})
|
|
|
|
t.Run("System users are always upserted", func(t *testing.T) {
|
|
ctx := t.Context()
|
|
sysUser := services.NewSystemAutomaticAccessBotUser(testModules.BuildType()).(*types.UserV2)
|
|
|
|
// GIVEN a user database...
|
|
manager := newMockUserManager(t)
|
|
|
|
// Set the expectation that all user creations will succeed EXCEPT
|
|
// for our known system user
|
|
manager.On("CreateUser", mock.Anything, mock.Anything).
|
|
Run(requireSystemResource(t, 1)).
|
|
Maybe().
|
|
Return(sysUser, nil)
|
|
|
|
// All attempts to upsert should succeed, and record the being upserted
|
|
var upsertedUsers []string
|
|
manager.On("UpsertUser", mock.Anything, mock.Anything).
|
|
Run(func(args mock.Arguments) {
|
|
u := args.Get(1).(types.User)
|
|
upsertedUsers = append(upsertedUsers, u.GetName())
|
|
}).
|
|
Return(sysUser, nil)
|
|
|
|
// WHEN I attempt to create the preset users...
|
|
err := auth.CreatePresetUsers(ctx, testModules.BuildType(), manager)
|
|
|
|
// EXPECT that the process succeeds and the system user was upserted
|
|
require.NoError(t, err)
|
|
manager.AssertExpectations(t)
|
|
require.Contains(t, upsertedUsers, sysUser.Metadata.Name)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDashboardMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
conf := setupConfig(t)
|
|
// dashboard mode is determined via cloud and recovery codes
|
|
conf.Modules = &modulestest.Modules{
|
|
TestBuildType: modules.BuildEnterprise,
|
|
TestFeatures: modules.Features{
|
|
Cloud: false,
|
|
RecoveryCodes: true,
|
|
},
|
|
}
|
|
ctx := t.Context()
|
|
authServer, err := auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = authServer.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
// verify auth server is functional
|
|
_, err = authServer.GetClusterName(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// verify that preset roles were NOT created in dashboard mode
|
|
presetRoles := auth.GetPresetRoles(conf.Modules.BuildType())
|
|
|
|
for _, role := range presetRoles {
|
|
_, err := authServer.GetRole(ctx, role.GetName())
|
|
require.True(t, trace.IsNotFound(err), "expected preset role %q to not exist in dashboard mode", role.GetName())
|
|
}
|
|
|
|
// verify preset users were NOT created in dashboard mode
|
|
presetUsers := auth.GetPresetUsers(conf.Modules.BuildType())
|
|
|
|
for _, user := range presetUsers {
|
|
_, err := authServer.GetUser(ctx, user.GetName(), false)
|
|
require.True(t, trace.IsNotFound(err), "expected preset user %q to not exist in dashboard mode", user.GetName())
|
|
}
|
|
}
|
|
|
|
func TestGetPresetUsers(t *testing.T) {
|
|
t.Parallel()
|
|
// no preset users for OSS
|
|
require.Empty(t, auth.GetPresetUsers(modules.BuildOSS))
|
|
|
|
// preset user @teleport-access-approval-bot on enterprise
|
|
require.Equal(t, []types.User{
|
|
services.NewSystemAutomaticAccessBotUser(modules.BuildEnterprise),
|
|
}, auth.GetPresetUsers(modules.BuildEnterprise))
|
|
}
|
|
|
|
type mockUserManager struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func newMockUserManager(t *testing.T) *mockUserManager {
|
|
m := &mockUserManager{}
|
|
m.Test(t)
|
|
return m
|
|
}
|
|
|
|
func (m *mockUserManager) CreateUser(ctx context.Context, user types.User) (types.User, error) {
|
|
type delegateFn = func(context.Context, types.User) (types.User, error)
|
|
args := m.Called(ctx, user)
|
|
if delegate, ok := args.Get(0).(delegateFn); ok {
|
|
return delegate(ctx, user)
|
|
}
|
|
return args.Get(0).(types.User), args.Error(1)
|
|
}
|
|
|
|
func (m *mockUserManager) GetUser(ctx context.Context, username string, withSecrets bool) (types.User, error) {
|
|
type delegateFn = func(ctx context.Context, username string, withSecrets bool) (types.User, error)
|
|
args := m.Called(ctx, username, withSecrets)
|
|
if delegate, ok := args.Get(0).(delegateFn); ok {
|
|
return delegate(ctx, username, withSecrets)
|
|
}
|
|
return args.Get(0).(types.User), args.Error(1)
|
|
}
|
|
|
|
func (m *mockUserManager) UpsertUser(ctx context.Context, user types.User) (types.User, error) {
|
|
type delegateFn = func(context.Context, types.User) (types.User, error)
|
|
args := m.Called(ctx, user)
|
|
if delegate, ok := args.Get(0).(delegateFn); ok {
|
|
return delegate(ctx, user)
|
|
}
|
|
return args.Get(0).(types.User), args.Error(1)
|
|
}
|
|
|
|
var _ auth.PresetUsers = &mockUserManager{}
|
|
|
|
type mockRoleManager struct {
|
|
mock.Mock
|
|
}
|
|
|
|
var _ auth.PresetRoleManager = &mockRoleManager{}
|
|
|
|
func newMockRoleManager(t *testing.T) *mockRoleManager {
|
|
m := &mockRoleManager{}
|
|
m.Test(t)
|
|
return m
|
|
}
|
|
|
|
// CreateRole creates a role.
|
|
func (m *mockRoleManager) CreateRole(ctx context.Context, role types.Role) (types.Role, error) {
|
|
type delegateFn = func(context.Context, types.Role) (types.Role, error)
|
|
args := m.Called(ctx, role)
|
|
if delegate, ok := args[0].(delegateFn); ok {
|
|
return delegate(ctx, role)
|
|
}
|
|
if args[0] == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args[0].(types.Role), args.Error(1)
|
|
}
|
|
|
|
func (m *mockRoleManager) GetRole(ctx context.Context, name string) (types.Role, error) {
|
|
type delegateFn = func(context.Context, string) (types.Role, error)
|
|
args := m.Called(ctx, name)
|
|
if delegate, ok := args[0].(delegateFn); ok {
|
|
return delegate(ctx, name)
|
|
}
|
|
if args[0] == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args[0].(types.Role), args.Error(1)
|
|
}
|
|
|
|
func (m *mockRoleManager) UpsertRole(ctx context.Context, role types.Role) (types.Role, error) {
|
|
type delegateFn = func(context.Context, types.Role) (types.Role, error)
|
|
args := m.Called(ctx, role)
|
|
if delegate, ok := args[0].(delegateFn); ok {
|
|
return delegate(ctx, role)
|
|
}
|
|
return args[0].(types.Role), args.Error(1)
|
|
}
|
|
|
|
func requireSystemResource(t *testing.T, argno int) func(mock.Arguments) {
|
|
return func(args mock.Arguments) {
|
|
argOfInterest := args[argno]
|
|
require.Implements(t, (*types.Resource)(nil), argOfInterest)
|
|
require.True(t, types.IsSystemResource(argOfInterest.(types.Resource)))
|
|
}
|
|
}
|
|
|
|
func setupConfig(t *testing.T) auth.InitConfig {
|
|
tempDir := t.TempDir()
|
|
|
|
bk, err := memory.New(memory.Config{})
|
|
require.NoError(t, err)
|
|
|
|
processStorage, err := storage.NewProcessStorage(
|
|
context.Background(),
|
|
filepath.Join(tempDir, teleport.ComponentProcess),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
|
|
ClusterName: "me.localhost",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
bk.Close()
|
|
processStorage.Close()
|
|
})
|
|
|
|
authority, err := testauthority.NewKeygen(modules.BuildOSS, time.Now)
|
|
require.NoError(t, err)
|
|
|
|
return auth.InitConfig{
|
|
DataDir: tempDir,
|
|
HostUUID: "00000000-0000-0000-0000-000000000000",
|
|
NodeName: "foo",
|
|
Backend: bk,
|
|
VersionStorage: processStorage,
|
|
Authority: authority,
|
|
ClusterAuditConfig: types.DefaultClusterAuditConfig(),
|
|
ClusterNetworkingConfig: types.DefaultClusterNetworkingConfig(),
|
|
SessionRecordingConfig: types.DefaultSessionRecordingConfig(),
|
|
ClusterName: clusterName,
|
|
StaticTokens: types.DefaultStaticTokens(),
|
|
AuthPreference: types.DefaultAuthPreference(),
|
|
SkipPeriodicOperations: true,
|
|
Tracer: tracing.NoopTracer(teleport.ComponentAuth),
|
|
}
|
|
}
|
|
|
|
func newWebauthnAuthPreferenceConfigFromFile(t *testing.T) types.AuthPreference {
|
|
ap, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
|
|
Type: constants.Local,
|
|
SecondFactor: constants.SecondFactorWebauthn,
|
|
Webauthn: &types.Webauthn{
|
|
RPID: "localhost",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return ap
|
|
}
|
|
|
|
// Example resources generated using `tctl get all --with-secrets`.
|
|
const (
|
|
hostCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: 6cc380df-816d-4c6b-8fc8-c20af75eede2
|
|
spec:
|
|
active_keys:
|
|
ssh:
|
|
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSU5WTzBiNUxSOXk2Nm5SRGJHN3JJUzFRZ3dBcUVpSWtMZS9WVmFrd3pJZ2oKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
|
|
public_key: c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSUw2ZWtVSTg3U3VOYkFiWnhPbGxRUEJJWGdPVjFNcEt4UWVNQXB0MklpVlYK
|
|
tls:
|
|
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNBakNDQWFlZ0F3SUJBZ0lSQUpKYkRNWGtYN3ZOR0EreFRQeGlzYzR3Q2dZSUtvWkl6ajBFQXdJd1lERVYKTUJNR0ExVUVDaE1NYldVdWJHOWpZV3hvYjNOME1SVXdFd1lEVlFRREV3eHRaUzVzYjJOaGJHaHZjM1F4TURBdQpCZ05WQkFVVEp6RTVORFUwTURBME5UUTJOakkyTlRrMk1qazBOakkxTURNNU5UYzJNamM1TkRBNE1qYzJOakFlCkZ3MHlOREV5TXpFeE5ESTJNRFphRncwek5ERXlNamt4TkRJMk1EWmFNR0F4RlRBVEJnTlZCQW9UREcxbExteHYKWTJGc2FHOXpkREVWTUJNR0ExVUVBeE1NYldVdWJHOWpZV3hvYjNOME1UQXdMZ1lEVlFRRkV5Y3hPVFExTkRBdwpORFUwTmpZeU5qVTVOakk1TkRZeU5UQXpPVFUzTmpJM09UUXdPREkzTmpZd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxCmhrak9QUU1CQndOQ0FBUzZTZE92WEdZV2wyY2FCRHFPcXRRN3RyNW1JWkdKR0JFYXJWUnMvSFFvYXpiZExKK0IKMExMUFhHemQvOTNxZ04wNXJVWGhyNHVXb3pEeTMxR2V4ZDZmbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQVlZdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVLWmhpWVRwc3Z0NTkwTXc1OGhPY2xNcTBVYmN3CkNnWUlLb1pJemowRUF3SURTUUF3UmdJaEFLcFExNWt6MFZlQThmOEE5S3RKRS9LLytxVkkyNGFpMnM5dytmYTQKVFNFdkFpRUEwWWtNZGNFc1dqTjBFeFpzVmVsRU5EdS9hcWhtUkpua2FpakJDNjFBY0Q0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
|
key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ3RTcVllSWhXeTZ3S3QxVDYKUVdVSitTVkF0SjRpNkppajhac0hmWkw2YzJXaFJBTkNBQVM2U2RPdlhHWVdsMmNhQkRxT3F0UTd0cjVtSVpHSgpHQkVhclZScy9IUW9hemJkTEorQjBMTFBYR3pkLzkzcWdOMDVyVVhocjR1V296RHkzMUdleGQ2ZgotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: host
|
|
sub_kind: host
|
|
version: v2`
|
|
userCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: b82425a2-dd10-49c6-9b8a-3f10e3ec8a41
|
|
spec:
|
|
active_keys:
|
|
ssh:
|
|
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSU1tK2EyVlRqSHV2OVhrWG1UR2Roem5GSVJPK0pTaVEyWUF6eHNleEJFZGEKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
|
|
public_key: c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSUtZYU1pUWZTUWgyQVpqN2lNSEFBQUpxU0FQWWJLL0gzRFJ3NWtUSXBDRUMK
|
|
tls:
|
|
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNBakNDQWFlZ0F3SUJBZ0lSQUpsUkc4OFlqTURVUmZmOENYMnRpbkF3Q2dZSUtvWkl6ajBFQXdJd1lERVYKTUJNR0ExVUVDaE1NYldVdWJHOWpZV3hvYjNOME1SVXdFd1lEVlFRREV3eHRaUzVzYjJOaGJHaHZjM1F4TURBdQpCZ05WQkFVVEp6SXdNemM1TXpBeU16UXpNelV5TURFNE9URXdNRFU0TnpNek9EWXlNalkxT1RVMk1qQTVOakFlCkZ3MHlOREV5TXpFeE5ESTJNRFphRncwek5ERXlNamt4TkRJMk1EWmFNR0F4RlRBVEJnTlZCQW9UREcxbExteHYKWTJGc2FHOXpkREVWTUJNR0ExVUVBeE1NYldVdWJHOWpZV3hvYjNOME1UQXdMZ1lEVlFRRkV5Y3lNRE0zT1RNdwpNak0wTXpNMU1qQXhPRGt4TURBMU9EY3pNemcyTWpJMk5UazFOakl3T1RZd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxCmhrak9QUU1CQndOQ0FBUlN0T0tLYklVeGppSTFIZEJGU2xVQW1LQW0wUXlxejljWVZEdlZEc0RNT1BqNFRTWGsKOHp5Q0FhYXloTlgra21lZlU3R2ZIeDBlVVMxbVhBYnl6UnNHbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQVlZdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVKZi94RjBtMStZM1lIRlRubVJhcGJ1dVdqa293CkNnWUlLb1pJemowRUF3SURTUUF3UmdJaEFLZkU5bXpPdzcrcUx3bEFxdFFWUVBNODVFTGp2NlFSdmZrcUE3aWoKSEtCMUFpRUFoNjVJZmV4NFpwT0szYVVUVTZQWGFkODlDdmhVb3NFMzRzWU1FZHdxZk9zPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
|
key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZzRXUVZQclNGTmR6bVFMRWcKQkMvTU5sb2JEL0dKekNGU01vOUZsV0pjZDY2aFJBTkNBQVJTdE9LS2JJVXhqaUkxSGRCRlNsVUFtS0FtMFF5cQp6OWNZVkR2VkRzRE1PUGo0VFNYazh6eUNBYWF5aE5YK2ttZWZVN0dmSHgwZVVTMW1YQWJ5elJzRwotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: user
|
|
sub_kind: user
|
|
version: v2`
|
|
databaseClientCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: 797ca33f-e922-45db-b531-ab62af23963c
|
|
spec:
|
|
active_keys:
|
|
tls:
|
|
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURqRENDQW5TZ0F3SUJBZ0lRWGtnVThDZFo3TnkrbVJacG9GZUwzVEFOQmdrcWhraUc5dzBCQVFzRkFEQmcKTVJVd0V3WURWUVFLRXd4dFpTNXNiMk5oYkdodmMzUXhGVEFUQmdOVkJBTVRERzFsTG14dlkyRnNhRzl6ZERFdwpNQzRHQTFVRUJSTW5NVEkxTXpJeE56QXhOalV5TnpJMk16QTBORE13TlRRM05EQTNNVEE0TXpjM05qUXpPVGszCk1CNFhEVEkwTVRJek1URTBNall3TmxvWERUTTBNVEl5T1RFME1qWXdObG93WURFVk1CTUdBMVVFQ2hNTWJXVXUKYkc5allXeG9iM04wTVJVd0V3WURWUVFERXd4dFpTNXNiMk5oYkdodmMzUXhNREF1QmdOVkJBVVRKekV5TlRNeQpNVGN3TVRZMU1qY3lOak13TkRRek1EVTBOelF3TnpFd09ETTNOelkwTXprNU56Q0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMMDZCa0hXMTBpWm1NTHZ6Y01tNC9WNmk3M2EvWjNpM1hzN1IxamYKc0FDQlB5TkNJTkFCbnFoV0NHdWJPb0JWT3REemQyVUJTZ0JXYVVNcXhiUytmQjBLYllEVDVkaFN1YXd4Wk8zQwpmN0xaZWtlZk4vb1FZSjk2WlVoemVvL1g4dkZZNE5ocXBIbXJMV1VRTGpZUEZ5cVhIWGM1L3hTUkVPVm8ybnZyCjNOWXFnR2EwRUFvMTRvRUJsR20zMmNQUWNvTGRBUUhqaFJrampQYStNeXRGSmNzeFkxWTFDemM5V1hnYzZvK1AKRWp0U3dRMW9Fd3ZOVHhYbXp1UElOcTZxdVZjS1kwcDF0anBWT1R1dWpaZ1R5NGRIMkJIQ1I5d1ZxejlVRGJPWgpWVHVKWjVnRFdQQkpGcVRPNUhkbzh5QnVLOXM0RndYZDJoaC9oa1YrNitabXdJRUNBd0VBQWFOQ01FQXdEZ1lEClZSMFBBUUgvQkFRREFnR21NQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGRTZ4R09aSmFoTzUKc3FmQ3pZK0ZxTjQ3eGpWeE1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQk41V2JLdVRzaWlvUlltS3BlcGtiTQppT2FCbGJ4ajZtOTZDVHZpczJSeXlmUU1qSXduT1dYbUtMMHpGQUtvZThDWVk0UEJFWEtuZzZBd0NNVXdzWnNPCkZPMFE2Q21JUzlwQXdVZURIZ3VrN2x2dWQyeGFvb0MrazY4OGtmUlZhRjVPTW5WaW4xajBJY1MwM1d1QlhSckwKVDVwODFza3JOaG9Gd1BtdnU0RkZmU1lHb1YrajQ4NC83Y0FOY3J2c0FFdFBFdk9EdzlUVG5vb05zdUoreVhaMAoyVW5LSG1rOTFCeGdZVDVHUE1CVkYwQWxzenhjcVN0S2hBMlhrdzBCY1pnTXVBa3BYSXJRbUZNNGh0RnIyY0NOCkpTTk1mVUpsZVlXLzJiNmJvbENZTUliTFoxeldaaDBVYTJ6QjlOME9wNE1aS25xRmhtc2JVWFFOa0xGd1FEYzAKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
|
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdlRvR1FkYlhTSm1Zd3UvTnd5Ymo5WHFMdmRyOW5lTGRlenRIV04rd0FJRS9JMElnCjBBR2VxRllJYTVzNmdGVTYwUE4zWlFGS0FGWnBReXJGdEw1OEhRcHRnTlBsMkZLNXJERms3Y0ovc3RsNlI1ODMKK2hCZ24zcGxTSE42ajlmeThWamcyR3FrZWFzdFpSQXVOZzhYS3BjZGR6bi9GSkVRNVdqYWUrdmMxaXFBWnJRUQpDalhpZ1FHVWFiZlp3OUJ5Z3QwQkFlT0ZHU09NOXI0ekswVWx5ekZqVmpVTE56MVplQnpxajQ4U08xTEJEV2dUCkM4MVBGZWJPNDhnMnJxcTVWd3BqU25XMk9sVTVPNjZObUJQTGgwZllFY0pIM0JXclAxUU5zNWxWTzRsbm1BTlkKOEVrV3BNN2tkMmp6SUc0cjJ6Z1hCZDNhR0grR1JYN3I1bWJBZ1FJREFRQUJBb0lCQUM4MnF4a0NZZlRiWGlKRgpjekdlSW9LOWNPQ09JM21oZ1dHZUNNOUVBTVlmZVlGeW5uMUg2aTVXU1FPUVY2aHRtNTlISUNNemp5TkdiRDAyCkR0NXFLTTJXTEh4WVlxRDNBeHpUdGpzY3JJQVRnMDhiaXZ2NTJpSHdpQlRydTBqb3VOVS9OOXJId1FJYWs5a0QKa0lRc2Y3dEF1VGxtWHg3aWt6U3FWTmxXb0dOUENZek9Bb3FZcXlTaWhEc0pLZ3N1SVFOV0lPVzE1dGxmb0NsQgpTTkF0cGxlclBNbFl6T2FjaUZVWkJFbGVOWEp0VURwd0szOGU5N2RMclJrYWRLeXhSVTNVTTE4dFR5TDZrZ2hMCmVsT3JBWDE2K0RFdm1PeDhGQlgyS29tak9OL0pORVZUN2lkcndyVnh2OU1PTUFpQjRDMms0a25jbUxIVE5DTE0KelJmOHY0RUNnWUVBNVg1d0VXVlZ0WFhNek02VVZCZ1ZwMGZmNVlPTTNTV3FoOHE1OGQrLzVBS0ZZSjZ5U1BpLwpoaVJvZ2xXc2NheXV3UVhTaHVMV2ZQVHJxMmVQNFVlcGdEa0g0c3NiamFwODFTMFZnN2JJZ3ZrdGxOTjdFMkRJCnFhMS9pNzBBQW9GOFNNY20vRE00N3RiTGxxS0ZwT21GSjB3dk1zUkh5aTJGM3pRaDB1aTFTQThDZ1lFQTB4VDgKSFZISnl1VHEvMERBdi9kLy9RNEtFUW5CS3ZmaFV5K0FOd0NGNkFhRW5USnAzTTRmNkpmUkVRUytFd1ZVVFk5WAoxWkNBdTd5N2NoQmtORG5TMmZJR2ZiQnYramVGWm9SZDRkQ3RwUnB1bHJka21yNGFaZnZnaGZsanJkdVZrdEM3CmZtQjZORTl5TURMYVBWT3Z4VEZGd3ZYMHRRUFdmYVNpL0Z3NVhtOENnWUVBdUtJT201QkJjbXBCeUl4eXZXMWIKRG1nKzg3SHdoSU1uUFhTV1FNaFk0Nkk3bUU1VTlXeGEraHNVa2JkSHMzVFFhNjY1ZjVmRUpHZ1BxcWo1RXEvSwo2TVA1V2pjNkJiR2lHUWZhaFV0cTZpUjZ6WCtQUnpuWWR0cUZBUEdmcm1ScWowcmFUSkVSUHVaRWlQNWNNeDlFCjV5YmQyaVFiOWNiR0s1c1BrMVZ4YzNVQ2dZRUFybkVzUGNyRzJyKyttYjVZelF6c29DUkhHM2VWUlQ1ZjM5QmsKeEkvUkdrU3d1ZnpjMGhjaTlhVHBxWWZpMFhOWkRWUUdRYi9QTTlld2pYNlFZVHpjVFRPZ082VmhsVWJuSHljTApNMEN6RUx3OFlxQWpLMk1xQzloUjRFYVBJekpTZFdlOVc1Njl2NWRjaGdxd28zZ1N6Z04vWkxUQlRBdGs2cWJ4CjcxOEVKazhDZ1lFQXZGckFPc1VJdmh4T0FYYk5mMUxwbnlabXVORUg3ek9XZ1FNM1ZlNG9iT3hqenVHaGs2RFUKVGhFdDg0emhsMk5LeFVacHRwdEJLRkdxM3B2dGc2TElqbEtpWmNPUEovbHcrTVZPb0U0S1Y5U0F3Q1VPN3o4eApDbTlzM05ZQTV6RWZRdGg0OEt5NmdBZVhZUHlkMWZidjl2QXlwM3FQbEpxa281SG5peHNxamNvPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: db_client
|
|
sub_kind: db_client
|
|
version: v2`
|
|
databaseCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: 3b019bb5-051c-461a-a7f4-e335fc031bf3
|
|
spec:
|
|
active_keys:
|
|
tls:
|
|
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURqVENDQW5XZ0F3SUJBZ0lSQUs5OUdYVGFmVEhNNWN2cEpzK0xhTkV3RFFZSktvWklodmNOQVFFTEJRQXcKWURFVk1CTUdBMVVFQ2hNTWJXVXViRzlqWVd4b2IzTjBNUlV3RXdZRFZRUURFd3h0WlM1c2IyTmhiR2h2YzNReApNREF1QmdOVkJBVVRKekl6TXpJMk5EUTFNalk0T0RBd016RTFOekl3TWpNeE16TTVOekl3T0RZMU5UVTFORGMyCk9UQWVGdzB5TkRFeU16RXhOREkyTURaYUZ3MHpOREV5TWpreE5ESTJNRFphTUdBeEZUQVRCZ05WQkFvVERHMWwKTG14dlkyRnNhRzl6ZERFVk1CTUdBMVVFQXhNTWJXVXViRzlqWVd4b2IzTjBNVEF3TGdZRFZRUUZFeWN5TXpNeQpOalEwTlRJMk9EZ3dNRE14TlRjeU1ESXpNVE16T1RjeU1EZzJOVFUxTlRRM05qa3dnZ0VpTUEwR0NTcUdTSWIzCkRRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRE8ybGRZbjhIc3ZNWTkzZVFMSWRoMUk5WmJtWWFlZm80UHJJRTgKNXA0bUpSUnVMMDg2Y0xlbFZNZDc0K1h3RmlSOUpLNlY1ZHBYVGw1cFoxTVBEbjh1cUkvMjAwWnlvVHlad1ZWUgpISUdwSjFqTVRZbjBQcDE0TC81Uk9MbXAwVTEzL3hRVDd4a2QxZnA4QVdzVzFYYnloc1hYaEhkMUpraTRFY0djCkJtSjc0Nml3aGxyUVNZcTNvSFpXNzZjdVVaM0g2bU9QVzVZenl1cUVYR3J3SUxneExZSVZNNXlxTUpEbHdiTU8KMzlBTGE4bzl1U1ZiaGJ1QjlaeG9BT1h4ck9ERGJSN0RuVTc0cEFTQm5pV2g5aTFEMTBBK2NlMEQySVNjTFcvWAo4MXpKSzVGSXRZSldSVDQvZUo3MmNMUGUycFRlSTMreEFCNW9tZVBCQjlrZjlLanRBZ01CQUFHalFqQkFNQTRHCkExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlFacW9pb3paYVoKMTVOcFMrVjVUV1RUNnY0S0V6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFPZFhnYk1XWUM0TFdmMnRtNUtyZQoyM2J5MlgxWHZYQ2U4L1RGdFVsMUNhdHVrV2MwY205cXUxckxiRy84ZUxCWjA2cUZSc1VTSEUxamEyRUtkREU0CjU5Y0p3bE5DN1hCMjRTQ2s1QTcxNGtpSDZMQjAxRk00WGEzWjg4YnZCSHV2ak01Q1ErN1lSSjlGZFd6YjFKK3UKREF3czI1V0haWTFGV0ovUGt5eTVycjJNTWVvS3dWMGRNSjJJWlZVMmpGZkVhYXhLWml4Y29EbG5KclIzWThEMwovUVZRckY5d0xkQWVENkExTlpjbnd3Q2F0MWFoOVA1cWhQdGlnc2lsOVM5dkd3T0JyQUEwNE9iZXQrVFNjd0tnCkVQQWhFWVlkRWUvRzV6MVZUQXBCUCtzeXRWcHdRZXQxZDRlV2VvVW04QU8yZEVuQWhhM0FvU1pmQjFTVVgxTmEKb3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
|
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBenRwWFdKL0I3THpHUGQza0N5SFlkU1BXVzVtR25uNk9ENnlCUE9hZUppVVViaTlQCk9uQzNwVlRIZStQbDhCWWtmU1N1bGVYYVYwNWVhV2RURHc1L0xxaVA5dE5HY3FFOG1jRlZVUnlCcVNkWXpFMkoKOUQ2ZGVDLytVVGk1cWRGTmQvOFVFKzhaSGRYNmZBRnJGdFYyOG9iRjE0UjNkU1pJdUJIQm5BWmllK09vc0laYQowRW1LdDZCMlZ1K25MbEdkeCtwamoxdVdNOHJxaEZ4cThDQzRNUzJDRlRPY3FqQ1E1Y0d6RHQvUUMydktQYmtsClc0VzdnZldjYUFEbDhhemd3MjBldzUxTytLUUVnWjRsb2ZZdFE5ZEFQbkh0QTlpRW5DMXYxL05jeVN1UlNMV0MKVmtVK1AzaWU5bkN6M3RxVTNpTi9zUUFlYUpuandRZlpIL1NvN1FJREFRQUJBb0lCQUgxbTZ2c2taeG1SWEJHWAptcStSQmp3RnpPZGRUTHA3ZUw1UjAwdkxkK2NpSloraStNSXlJWE9PMFJ6dmphK2VqT0o5UVlaSWdiVGFJdXg5Cm9tSUhaTjB4ZlkyaWlodm1XZW5ReGx0VkQ5b3ZxMnE0TzBFaVVLN1RVYmVGenpEL1hacTR2a0JUZklPVS9MVCsKMnlCTnF6M2VyTVE2WDMxYkIwem9IdHJyRi91SWNzNHNnZXFOTWFtU2JUY2I1ODRSbHhUNDA0MUdlWWdtNlZCRQpHWnpRSjJESXlQbEExZFhGUW5lcHpaaWRqQmdJMWJKS3lTMzVsekdjWVc0aGgvbVFhUVp5ZmRuS3Z4QmlsdUlBCjBxcnAvTEQ4QVp6RWxONVNMb2JtMXVEZEIrRW1RNllKbDEvdm5iWEY1QWFXSDN6b3U1OHJDcGFudDcraVJ6OGwKNFhuWjl0VUNnWUVBN1dPajhMSDBYVnFMcDMyTnNrK2xnNmYwRSttVUZnMU93eWZGSEJPdlpsRDQ1T3ZQNWx6ZgpsLzNoaUJiZEMzNHNhWU1HdW9GZlRhMjJiVHpwTXl6eXFhZVRhdy9HS3psR2Jlc0dIRk9Xd3F2U2Ivd2tWTEUvCkZocTZGSE1vWGp0aTE4eFhjRXdJQmVQaHhVZVkrZkd3V0RTREV1c3NZdFFxQjh1ajBXaDlYUDhDZ1lFQTN4SFgKbUhpdjFWLytPMkNPZDdWUkFWaThKTS9JZ0NQTk84L0o2SWRYTkNwKy8wNW10dEh0QnpyVFN0cXBGMlBSUWJEaApjYmpuYk05TGJTTVNKTHNtYndTOEhtT3MwSGQwSzU1aXdqRmFxNUdOT0xBQmlrOGMrU2Naa0d5YitxRHlGRU9UCkMzMThFSEg5SlZ6L24zYWxHZmRGWG9xWkZ1RXJvQWJCTkFEZlBoTUNnWUJsZHkxZmQvQ201a2pDOGx0YVY4aTcKR1ZLdUlDeDNzSUIxMGMzaVRsZXVOL1hxZ3hCOXVqeW56cEJUaHRJOFUxWFFVM3pRd3ZObFZGYWhJbVBheDkrQQp2R3U2V3llczJmSk1rU1F2ZjFyMUlsUDBJYVcxdlh6bGljNzNackZlZGF1dDZWMkdWamtucTF1WTR4MXoxK1kwCkRWM28vRFFnbWViTkpqR0RGRkpoS1FLQmdEaG1VSFp5ZlRLYjFMRzZsZ3JhUXlMdUJwUGdIVGVZMWJrN3JqY20Ka1B2VmlzcU9UaFlIT2NETU5NUUdTUjVxMUd1aGh6NnptMys5WWJxMFZWQUlLWTJFU3ZQOEM2T2hzRE9mRmlVMwpTVTk3dTVNTG5UZ1ZES1JLS0lLRmsySm84d3dBa2RzajNReGpaYmZlclpycDZwQ0lIbmZxM3c0VDNHM1hoMTNZCm9wa1ZBb0dCQU5USGlsZHVuWXBCTXNFVks5TFRWSm95TlNOTVdBbGFGaWlwZllmWm82M3RyQzgxK0FtMHFDQW4KZTF1V2NwRFFkQlZzbGRnTWV2TEcwOUNBU05rRjN0ZUJqWEsranNzWTRWMllTU1Bwd0dyWWhjZHpFMFBIZEVROApiMGllSGJUMGRZZ3hjWmczbW9pN1BDZi90d3Joa2lDdllIMlBMR2hzMkduV1JzSEQzcHY1Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: db
|
|
sub_kind: db
|
|
version: v2`
|
|
jwtCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: a3f0310c-80fe-4c43-95d5-ba3f3cf9aedc
|
|
spec:
|
|
active_keys:
|
|
jwt:
|
|
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ3dFaDQ2empZTHltT0ZqNWIKSVZDNXBkMUxSbWU1clAzTmRVN3hKQ0plMDJ1aFJBTkNBQVRYQXA5MFRRTmowaDhCNHllR0dnNFJWQStsbVV4MwpRN3EzdFBLbEJaSVpiMWtXSHdSRmFXdUJmYUQ3Sis0MnUwSjRvVGxqYU5zUUg2Vm8wRXRLTkI4ZwotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
|
|
public_key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFMXdLZmRFMERZOUlmQWVNbmhob09FVlFQcFpsTQpkME82dDdUeXBRV1NHVzlaRmg4RVJXbHJnWDJnK3lmdU5ydENlS0U1WTJqYkVCK2xhTkJMU2pRZklBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: jwt
|
|
sub_kind: jwt
|
|
version: v2`
|
|
openSSHCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: 2c49b2de-7529-4e8f-9442-dec2357f2e93
|
|
spec:
|
|
active_keys:
|
|
ssh:
|
|
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUhoT0o3KzZlek4raTZ5dVVhVDBPT01rVm1NK0pIZm41Sm5ONzJFNmduWTgKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
|
|
public_key: c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSVBuQ0ZocHk3TUdBZTNBSDBjTDBkMmVUK20xNVF1dmptMUU2QXlubm1uSkYK
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: openssh
|
|
sub_kind: openssh
|
|
version: v2`
|
|
samlCAYAML = `kind: cert_authority
|
|
metadata:
|
|
name: me.localhost
|
|
revision: 252e2c73-6805-48d4-8491-dde615ff8c47
|
|
spec:
|
|
active_keys:
|
|
tls:
|
|
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURqVENDQW5XZ0F3SUJBZ0lSQUtDdHRGNGZ1V0ZTSzJaNm1mb3VPcmt3RFFZSktvWklodmNOQVFFTEJRQXcKWURFVk1CTUdBMVVFQ2hNTWJXVXViRzlqWVd4b2IzTjBNUlV3RXdZRFZRUURFd3h0WlM1c2IyTmhiR2h2YzNReApNREF1QmdOVkJBVVRKekl4TXpVM09EUXdORGszTXpFd056RTBORFkxTWpJMk9EVXlOelV5T0Rjek1qRTBOak0yCk1UQWVGdzB5TkRFeU16RXhOREkyTURaYUZ3MHpOREV5TWpreE5ESTJNRFphTUdBeEZUQVRCZ05WQkFvVERHMWwKTG14dlkyRnNhRzl6ZERFVk1CTUdBMVVFQXhNTWJXVXViRzlqWVd4b2IzTjBNVEF3TGdZRFZRUUZFeWN5TVRNMQpOemcwTURRNU56TXhNRGN4TkRRMk5USXlOamcxTWpjMU1qZzNNekl4TkRZek5qRXdnZ0VpTUEwR0NTcUdTSWIzCkRRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRREVBQTFtbG1jeU5uK3BiYzVtRkxVRWNFN3JSWWtxNlEydnNKdnYKd0F3dDRpbjhGSmoxdVJVenU1YUJqYUs5bERDN1cyd2JwSjI4SGZuZmtkb2ttOHN4T0xqTTh5YUlaSzBYNG9ZcwpTTDFhdFVPMUsrUkt5citJNGc0dzJBd05QUURsUE5xU0t3dlNzRXhsQzhSSXpUTllZYXVpQVUxaWdoVW5zZTUxCm5jMVRTbzJ0NStuemNoVEt2SU1qV0U5TUpmZ2lBRDRjVXlHVU5DQXV1dTRndWJEZVBFdStYZmVhUjNCcXJoNUwKaTRiWGdQanF3NnBLMTZzY1h5QlYwL1RzdGsxYXhpekh3M2h4SG5INDNFeUtsUVNneXBPNnNLTFFmR0VEVTZ6UgpjdlFIOHNOc0g0YmVNL2Zzb0RoeXFZQVZKbDVNT011UG0zTHBpdHJ1YktEOXA4S1ZBZ01CQUFHalFqQkFNQTRHCkExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlJqNytSM3pabEoKRlpPU05pM2xsOHhUclNQNVREQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFwZmJHWXA5Q1hoNksvL1FKY3kyUQpqVjF0RDk1akZYcVVyZ0JrZ1g0cmplaEVqQjd0N3k1SnNXZlJIK25DL2UxaGRqL3dlZWMxenR1MFJNazUxRThlCmJCNWFaLzV1eVhMWXlMVGJWaVpZQzEzV0EyTlZpY200d08yWDQvK29NNzVQTzFwUmRvTm4weGRlcDFPa2o2VDcKQlA3WVM3SUhYRlMwUE00bUQxRDRsTHRIQmhqcjdJQnNlK2ZSRXBaZEJZMktqOTZ3NjZzT0c1RlNNMVY0NWRiNQoyY1lhTHFEcGR3aFVEY0xDZWVIVnYzaTB4UGk2TUJmWTFoVGd1MjRTMFpHZnhpR2F5Qk5FWEpKL1dtNk5kQkFUClhVckV2YkU3Z2tORWRkeXJPUW9sWDA5R3lYU0dNbDVXMTVtMHNVaythNk5RanlVQTVxZU1sTTNWN1Q1REVLcWgKV0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
|
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBeEFBTlpwWm5NalovcVczT1poUzFCSEJPNjBXSkt1a05yN0NiNzhBTUxlSXAvQlNZCjlia1ZNN3VXZ1kyaXZaUXd1MXRzRzZTZHZCMzUzNUhhSkp2TE1UaTR6UE1taUdTdEYrS0dMRWk5V3JWRHRTdmsKU3NxL2lPSU9NTmdNRFQwQTVUemFraXNMMHJCTVpRdkVTTTB6V0dHcm9nRk5Zb0lWSjdIdWRaM05VMHFOcmVmcAo4M0lVeXJ5REkxaFBUQ1g0SWdBK0hGTWhsRFFnTHJydUlMbXczanhMdmwzM21rZHdhcTRlUzR1RzE0RDQ2c09xClN0ZXJIRjhnVmRQMDdMWk5Xc1lzeDhONGNSNXgrTnhNaXBVRW9NcVR1ckNpMEh4aEExT3MwWEwwQi9MRGJCK0cKM2pQMzdLQTRjcW1BRlNaZVREakxqNXR5NllyYTdteWcvYWZDbFFJREFRQUJBb0lCQUZtSU1KYnBJM0REaG1OdAozbmV4QTlOb1BoU282ZlNwQ3ZCemUzZjBRVndBVU85dXRVU2g3RFo2ZlZEbTB5MUljVTVVZjdqTTVLVFhDSnFBCjlLWCthTDR1Uy9TTEtkSHFNMHVTMVhtTExMd3Z5eU1LVHJsL2ppaklJblZiYTMzc25Pa2FlRG1HNGxxMjM5N1UKbGpBdlZFSU9NNm5JY0lJTUsvKzYvdFBKWnM2aGxXZXAzcGRuOWMwUXBUWGExbGpiZ1FVMHVNN2NwanRLRE4rcwpYRGRzb2N3VGpUV2MwNGY0aDBtS3NkdmlxeG80bVBSK0tyV3Brb1pFdFhiUGQ4THg5OVNkQjJ0aEs2enBiNEErCmZKOFlXYjhZTWp3SGpXRW5OYVpiM2FocGVuRkVyVWZFb1hlRGc4ajltak1VSGFSUURIK3ZGN0RINlI3U3lRb1QKOWo2dFBXRUNnWUVBemptNCtKamswbEd3SmVDMzAyN01Fd2lQMlNwcG0yRlNLY1MzVnM0cmE3TGhFR1pSZEpBcQpYSDNpdEVjZWViZTg4aU1RcDJXam1DMktEN1ZLOTBTN2pVUUpLbEw2eTM5WDh0RGlJRHQ4Y3l2OVd6TTJEK0p2Cm0vc3BFc1A4aURJTEVYZDJYUk1JNHhja2tXRml0TkE0SDNnODNtYWRPVWkzOEsyN0xyR3Q0b2tDZ1lFQTgwNkgKbWJOY1hKSTRML09FdTdHd2hVSk1uOGdLaWpvWStMdmZvQlN5Ym9SdzFIWkNvYnRZM2NLd0xNSnAvNkdQVG1lbwpucWlXNWs0YjJLVlpuczl0elNSRWhZa1NNTWRyZllhTis3bGZKTEozS3BhenNXMGlWbk5HaFlidzZVSnY0TmpnCjMzYnQvemh4bkp5ckhzS3B0VndiWm9lWmN2RDFkcVB5cm5kOVRLMENnWUVBdDkvYnZ6eUQrY3NBSmlYQmdmR3UKWCtJb2NGZFNwa29WK2t2OXRKWkxQTkhYdnNtY0l6UlBzUHhGWUx4d3ZkSkgxQlhUeVkza1dkRnc0aVNoWE91WgoxcEV0SXVHdDREZ0E4TzJ5VVU3NDNiQUJUSW5TMEVMemhMNWlsdXJNaFpzcEp6Kys5Nm43S0kvLytPZytIRDN6CmJJdkdxZjRRZlgwTEZMdXl4Q1dFaHhFQ2dZQVk2eG9JSzg1eHpLZmtnVlEreE53SFNkci9Ja1d5RW5Fc1NGR0cKMjVmS3FkWEViTGcyU0RHNXhJNjJodExFVTQrUndCd000OGRRbnY5TEdPUXMxNkd2T04rcnJYWW5lTVVSZmc1YwprWWVsQW9JaDRuMVUxcENGdWhpbTVFTVlJSzNFb1hHbWNVKytxOUUyOFBTMW1jbzN3TTh0bVFXbU4vZHJ4eTY3Cm41RTlvUUtCZ0R4VmptWGpCd1hkY3RhQm8ra3JHT1JWV2hJUzVWUFQrVkgwQ1F2MTUxcCtvT2EyMldQNSttbSsKUjJrcEhWNXRVTmUyUE9VZU5oMVE5U3JKQ1NLWmQ2K0JsT0U4bjQvc0kyNDJvbmRnelhrL21LRnc4NjBMQmdUbApkWTRvTC9mUU1mQ242aU4vMXBGZmFhWDZSV1lVMUplSTduK3VFZ0hDTFE4Z2x3eU1JcFdmCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
|
|
additional_trusted_keys: {}
|
|
cluster_name: me.localhost
|
|
type: saml_idp
|
|
sub_kind: saml_idp
|
|
version: v2`
|
|
healthCheckConfigYAML = `kind: health_check_config
|
|
metadata:
|
|
name: valid
|
|
revision: c1159783-930e-4a79-bb19-0d0d866d7af6
|
|
spec:
|
|
enabled: true
|
|
match:
|
|
db_labels:
|
|
- name: "*"
|
|
values:
|
|
- "*"
|
|
version: v1`
|
|
)
|
|
|
|
func TestInit_bootstrap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
hostCA := resourceFromYAML(t, hostCAYAML).(types.CertAuthority)
|
|
userCA := resourceFromYAML(t, userCAYAML).(types.CertAuthority)
|
|
jwtCA := resourceFromYAML(t, jwtCAYAML).(types.CertAuthority)
|
|
dbServerCA := resourceFromYAML(t, databaseCAYAML).(types.CertAuthority)
|
|
dbClientCA := resourceFromYAML(t, databaseClientCAYAML).(types.CertAuthority)
|
|
osshCA := resourceFromYAML(t, openSSHCAYAML).(types.CertAuthority)
|
|
samlCA := resourceFromYAML(t, samlCAYAML).(types.CertAuthority)
|
|
|
|
invalidHostCA := resourceFromYAML(t, hostCAYAML).(types.CertAuthority)
|
|
invalidHostCA.(*types.CertAuthorityV2).Spec.ActiveKeys.SSH = nil
|
|
invalidUserCA := resourceFromYAML(t, userCAYAML).(types.CertAuthority)
|
|
invalidUserCA.(*types.CertAuthorityV2).Spec.ActiveKeys.SSH = nil
|
|
invalidJWTCA := resourceFromYAML(t, jwtCAYAML).(types.CertAuthority)
|
|
invalidJWTCA.(*types.CertAuthorityV2).Spec.ActiveKeys.JWT = nil
|
|
invalidDBServerCA := resourceFromYAML(t, databaseCAYAML).(types.CertAuthority)
|
|
invalidDBServerCA.(*types.CertAuthorityV2).Spec.ActiveKeys.TLS = nil
|
|
invalidDBClientCA := resourceFromYAML(t, databaseClientCAYAML).(types.CertAuthority)
|
|
invalidDBClientCA.(*types.CertAuthorityV2).Spec.ActiveKeys.TLS = nil
|
|
invalidOSSHCA := resourceFromYAML(t, openSSHCAYAML).(types.CertAuthority)
|
|
invalidOSSHCA.(*types.CertAuthorityV2).Spec.ActiveKeys.SSH = nil
|
|
invalidSAMLCA := resourceFromYAML(t, samlCAYAML).(types.CertAuthority)
|
|
invalidSAMLCA.(*types.CertAuthorityV2).Spec.ActiveKeys.TLS = nil
|
|
|
|
tests := []struct {
|
|
name string
|
|
modifyConfig func(*auth.InitConfig)
|
|
assertError require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
// Issue https://github.com/gravitational/teleport/issues/7853.
|
|
name: "OK bootstrap CAs",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
userCA.Clone(),
|
|
jwtCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
dbClientCA.Clone(),
|
|
osshCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "OK bootstrap health check config",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
newHealthCheckConfig(t),
|
|
)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "NOK bootstrap health check config invalid",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
newHealthCheckConfig(t, func(hcc *healthcheckconfigv1.HealthCheckConfig) {
|
|
hcc.GetSpec().SetHealthyThreshold(9000)
|
|
}),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap Host CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
invalidHostCA.Clone(),
|
|
userCA.Clone(),
|
|
jwtCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
dbClientCA.Clone(),
|
|
osshCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap User CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
invalidUserCA.Clone(),
|
|
jwtCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
dbClientCA.Clone(),
|
|
osshCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap JWT CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
userCA.Clone(),
|
|
invalidJWTCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
dbClientCA.Clone(),
|
|
osshCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap Database CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
userCA.Clone(),
|
|
jwtCA.Clone(),
|
|
invalidDBServerCA.Clone(),
|
|
osshCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap Database Client CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
userCA.Clone(),
|
|
jwtCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
invalidDBClientCA.Clone(),
|
|
osshCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap OpenSSH CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
userCA.Clone(),
|
|
jwtCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
dbClientCA.Clone(),
|
|
invalidOSSHCA.Clone(),
|
|
samlCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "NOK bootstrap SAML IdP CA missing keys",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.BootstrapResources = append(
|
|
cfg.BootstrapResources,
|
|
hostCA.Clone(),
|
|
userCA.Clone(),
|
|
jwtCA.Clone(),
|
|
dbServerCA.Clone(),
|
|
dbClientCA.Clone(),
|
|
osshCA.Clone(),
|
|
invalidSAMLCA.Clone(),
|
|
)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := setupConfig(t)
|
|
test.modifyConfig(&cfg)
|
|
|
|
_, err := auth.Init(context.Background(), cfg)
|
|
test.assertError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
const (
|
|
userYAML = `kind: user
|
|
version: v2
|
|
metadata:
|
|
name: myuser
|
|
spec:
|
|
roles: ["admin"]`
|
|
tokenYAML = `kind: token
|
|
version: v2
|
|
metadata:
|
|
name: github-token
|
|
expires: "3000-01-01T00:00:00Z"
|
|
spec:
|
|
roles: [Bot]
|
|
join_method: github
|
|
bot_name: github-demo
|
|
github:
|
|
allow:
|
|
- repository: gravitational/example`
|
|
badTokenYAML = `kind: token
|
|
version: v2
|
|
metadata:
|
|
name: iam-token-without-orgid-but-ous
|
|
expires: "3000-01-01T00:00:00Z"
|
|
spec:
|
|
roles: [Node]
|
|
join_method: iam
|
|
allow:
|
|
- aws_account: "123456789012"
|
|
aws_organizational_units:
|
|
include: [r-rootid]`
|
|
boundKeypairTokenYAML = `kind: token
|
|
version: v2
|
|
metadata:
|
|
name: bound-keypair-token
|
|
expires: "3000-01-01T00:00:00Z"
|
|
spec:
|
|
roles: [Bot]
|
|
join_method: bound_keypair
|
|
bot_name: bound-keypair-demo
|
|
bound_keypair:
|
|
onboarding:
|
|
initial_public_key: "ssh-ed25519 AAAAtestinitialpublickey"
|
|
recovery:
|
|
limit: 1
|
|
mode: insecure`
|
|
// boundKeypairTokenNoRecoveryYAML omits spec.bound_keypair.recovery
|
|
// entirely; CheckAndSetDefaults must default it (limit 1) at unmarshal time,
|
|
// before apply-on-startup validation runs.
|
|
boundKeypairTokenNoRecoveryYAML = `kind: token
|
|
version: v2
|
|
metadata:
|
|
name: bound-keypair-token
|
|
expires: "3000-01-01T00:00:00Z"
|
|
spec:
|
|
roles: [Bot]
|
|
join_method: bound_keypair
|
|
bot_name: bound-keypair-demo
|
|
bound_keypair:
|
|
onboarding:
|
|
initial_public_key: "ssh-ed25519 AAAAtestinitialpublickey"`
|
|
roleYAML = `kind: role
|
|
version: v7
|
|
metadata:
|
|
name: admin
|
|
expires: "3000-01-01T00:00:00Z"
|
|
spec:
|
|
allow:
|
|
logins: ['admin']
|
|
kubernetes_groups: ['edit']
|
|
node_labels:
|
|
'*': '*'
|
|
kubernetes_labels:
|
|
'*': '*'
|
|
kubernetes_resources:
|
|
- kind: '*'
|
|
namespace: '*'
|
|
name: '*'
|
|
verbs: ['*']`
|
|
lockYAML = `
|
|
kind: lock
|
|
version: v2
|
|
metadata:
|
|
name: b1c785d8-8165-41fc-8dd6-252d534334d3
|
|
spec:
|
|
created_at: "2023-11-07T18:44:35.361806Z"
|
|
created_by: Admin
|
|
target:
|
|
user: myuser
|
|
`
|
|
clusterNetworkingConfYAML = `
|
|
kind: cluster_networking_config
|
|
metadata:
|
|
name: cluster-networking-config
|
|
spec:
|
|
proxy_listener_mode: 1
|
|
`
|
|
authPrefYAML = `
|
|
kind: cluster_auth_preference
|
|
metadata:
|
|
name: cluster-auth-preference
|
|
spec:
|
|
second_factor: off
|
|
type: local
|
|
version: v2
|
|
`
|
|
workloadIdentityYAML = `kind: workload_identity
|
|
version: v1
|
|
metadata:
|
|
name: example-workload-identity
|
|
spec:
|
|
spiffe:
|
|
id: /svc/example
|
|
`
|
|
botYAML = `kind: bot
|
|
metadata:
|
|
name: my-bot
|
|
spec:
|
|
roles: ["admin"]
|
|
`
|
|
)
|
|
|
|
func TestInit_ApplyOnStartup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
user := resourceFromYAML(t, userYAML).(types.User)
|
|
token := resourceFromYAML(t, tokenYAML).(types.ProvisionToken)
|
|
badToken := resourceFromYAML(t, badTokenYAML).(types.ProvisionToken)
|
|
role := resourceFromYAML(t, roleYAML).(types.Role)
|
|
lock := resourceFromYAML(t, lockYAML).(types.Lock)
|
|
clusterNetworkingConfig := resourceFromYAML(t, clusterNetworkingConfYAML).(types.ClusterNetworkingConfig)
|
|
authPref := resourceFromYAML(t, authPrefYAML).(types.AuthPreference)
|
|
workloadIdentity := resourceFromYAML(t, workloadIdentityYAML)
|
|
bot := resourceFromYAML(t, botYAML)
|
|
|
|
tests := []struct {
|
|
name string
|
|
modifyConfig func(*auth.InitConfig)
|
|
assertError require.ErrorAssertionFunc
|
|
}{
|
|
{
|
|
name: "Apply unsupported resource",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, lock)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "Apply invalid provision token",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, badToken)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
{
|
|
name: "Apply ProvisionToken",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply User (invalid, missing role)",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
|
|
},
|
|
assertError: require.Error,
|
|
},
|
|
// We test both user+role and role+user to validate that ordering doesn't matter
|
|
{
|
|
name: "Apply User+Role",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply Role+User",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply Role",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply ClusterNetworkingConfig",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, clusterNetworkingConfig)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply AuthPreference",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, authPref)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply WorkloadIdentity",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, workloadIdentity)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply Role+Bot",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, bot)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
{
|
|
name: "Apply Bot+Role",
|
|
modifyConfig: func(cfg *auth.InitConfig) {
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, bot)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
|
|
},
|
|
assertError: require.NoError,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := setupConfig(t)
|
|
test.modifyConfig(&cfg)
|
|
|
|
_, err := auth.Init(context.Background(), cfg)
|
|
test.assertError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInit_ApplyOnStartup_BoundKeypair verifies that bound_keypair tokens
|
|
// applied via --apply-on-startup have their status.bound_keypair initialized
|
|
// (the join path rejects tokens without it), and that re-applying on a
|
|
// subsequent auth restart preserves an already-populated status rather than
|
|
// resetting the bot's join state.
|
|
func TestInit_ApplyOnStartup_BoundKeypair(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("initializes status on first apply", func(t *testing.T) {
|
|
token := resourceFromYAML(t, boundKeypairTokenYAML).(types.ProvisionToken)
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stored.GetBoundKeypairStatus(),
|
|
"apply-on-startup must initialize status.bound_keypair, otherwise bound_keypair joins are rejected")
|
|
})
|
|
|
|
t.Run("applies a token with a defaulted recovery policy", func(t *testing.T) {
|
|
// A config that omits spec.bound_keypair.recovery must still apply:
|
|
// UnmarshalProvisionToken defaults recovery (limit 1) via
|
|
// CheckAndSetDefaults before apply-on-startup validation runs, so Init
|
|
// must not reject it.
|
|
token := resourceFromYAML(t, boundKeypairTokenNoRecoveryYAML).(*types.ProvisionTokenV2)
|
|
require.NotNil(t, token.Spec.BoundKeypair.Recovery,
|
|
"unmarshal must default recovery when the config omits it")
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = []types.Resource{token}
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint32(1), stored.GetBoundKeypair().Recovery.Limit,
|
|
"apply-on-startup must persist the defaulted recovery limit")
|
|
})
|
|
|
|
t.Run("discards config-supplied status on first apply", func(t *testing.T) {
|
|
// apply-on-startup config is spec-only and its status is untrusted. A
|
|
// bound_public_key supplied via config must not bind a key on creation:
|
|
// that would skip the join ceremony's proof-of-possession.
|
|
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
|
|
token.Status = &types.ProvisionTokenStatusV2{
|
|
BoundKeypair: &types.ProvisionTokenStatusV2BoundKeypair{
|
|
BoundPublicKey: "ssh-ed25519 AAAAconfigsuppliedkey",
|
|
RecoveryCount: 7,
|
|
},
|
|
}
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = []types.Resource{token}
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
status := stored.GetBoundKeypairStatus()
|
|
require.NotNil(t, status)
|
|
require.Empty(t, status.BoundPublicKey,
|
|
"config-supplied bound_public_key must not be persisted on first apply")
|
|
require.Zero(t, status.RecoveryCount,
|
|
"config-supplied recovery count must not be persisted on first apply")
|
|
})
|
|
|
|
t.Run("generates a registration secret when no onboarding credential is set", func(t *testing.T) {
|
|
// The Terraform onboarding flow supplies neither an initial_public_key
|
|
// nor an explicit registration_secret; apply-on-startup must generate
|
|
// the secret so the bot has a credential to join with.
|
|
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
|
|
token.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
|
token.Spec.BoundKeypair.Onboarding.RegistrationSecret = ""
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = []types.Resource{token}
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, stored.GetBoundKeypairStatus().RegistrationSecret,
|
|
"apply-on-startup must generate a registration secret when none is configured")
|
|
})
|
|
|
|
t.Run("preserves populated status on re-apply", func(t *testing.T) {
|
|
token := resourceFromYAML(t, boundKeypairTokenYAML).(types.ProvisionToken)
|
|
tokenV2 := token.(*types.ProvisionTokenV2)
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate a successful bound_keypair join populating the status.
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
storedV2, ok := stored.(*types.ProvisionTokenV2)
|
|
require.True(t, ok)
|
|
require.NotNil(t, storedV2.GetBoundKeypairStatus())
|
|
storedV2.Status.BoundKeypair.BoundPublicKey = "ssh-ed25519 AAAAboundpublickey"
|
|
storedV2.Status.BoundKeypair.RecoveryCount = 3
|
|
require.NoError(t, srv.UpsertToken(ctx, storedV2))
|
|
|
|
// Recovery policy is declarative and may change without changing the
|
|
// identity to which the key is bound.
|
|
tokenV2.Spec.BoundKeypair.Recovery.Limit = 5
|
|
|
|
// Re-running Init against the same backend re-applies the (spec-only)
|
|
// startup resource, mimicking an auth restart.
|
|
_, err = auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
reStored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
status := reStored.GetBoundKeypairStatus()
|
|
require.NotNil(t, status)
|
|
require.Equal(t, "ssh-ed25519 AAAAboundpublickey", status.BoundPublicKey,
|
|
"re-apply must preserve the bound public key")
|
|
require.Equal(t, uint32(3), status.RecoveryCount,
|
|
"re-apply must preserve the recovery count")
|
|
require.Equal(t, uint32(5), reStored.GetBoundKeypair().Recovery.Limit,
|
|
"re-apply must update the recovery policy")
|
|
})
|
|
|
|
t.Run("does not modify the registration secret on re-apply", func(t *testing.T) {
|
|
// Re-apply never modifies status. The stored registration secret is
|
|
// authoritative; editing spec.onboarding.registration_secret has no
|
|
// effect once the token exists.
|
|
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
|
|
token.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
|
token.Spec.BoundKeypair.Onboarding.RegistrationSecret = "old-registration-secret"
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = []types.Resource{token}
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
require.Equal(t, "old-registration-secret", stored.GetBoundKeypairStatus().RegistrationSecret)
|
|
|
|
updated := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
|
|
updated.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
|
|
updated.Spec.BoundKeypair.Onboarding.RegistrationSecret = "new-registration-secret"
|
|
cfg.ApplyOnStartupResources = []types.Resource{updated}
|
|
|
|
_, err = auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err = srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
require.Equal(t, "old-registration-secret", stored.GetBoundKeypairStatus().RegistrationSecret,
|
|
"re-apply must never modify status; the stored registration secret is authoritative")
|
|
})
|
|
|
|
t.Run("preserves status even when the spec identity changes", func(t *testing.T) {
|
|
// Re-apply never modifies status. Spec fields (including the bot) are
|
|
// applied freely; changing them does not unbind the key. To reset a
|
|
// binding, an admin must delete and recreate the token.
|
|
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
|
|
|
|
cfg := setupConfig(t)
|
|
cfg.ApplyOnStartupResources = []types.Resource{token}
|
|
|
|
srv, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err := srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
storedV2 := stored.(*types.ProvisionTokenV2)
|
|
storedV2.Status.BoundKeypair.BoundPublicKey = "ssh-ed25519 AAAAboundpublickey"
|
|
require.NoError(t, srv.UpsertToken(ctx, storedV2))
|
|
|
|
updated := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
|
|
updated.Spec.BotName = "different-bot"
|
|
cfg.ApplyOnStartupResources = []types.Resource{updated}
|
|
|
|
_, err = auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
stored, err = srv.GetToken(ctx, token.GetName())
|
|
require.NoError(t, err)
|
|
require.Equal(t, "different-bot", stored.(*types.ProvisionTokenV2).Spec.BotName,
|
|
"spec fields are applied freely")
|
|
require.Equal(t, "ssh-ed25519 AAAAboundpublickey", stored.GetBoundKeypairStatus().BoundPublicKey,
|
|
"re-apply must preserve status; changing spec never unbinds the key")
|
|
})
|
|
}
|
|
|
|
func resourceFromYAML(t *testing.T, value string) types.Resource {
|
|
t.Helper()
|
|
|
|
ur := &services.UnknownResource{}
|
|
err := kyaml.NewYAMLToJSONDecoder(strings.NewReader(value)).Decode(ur)
|
|
require.NoError(t, err)
|
|
|
|
resource, err := services.UnmarshalResource(ur.Kind, ur.Raw)
|
|
require.NoError(t, err)
|
|
return resource
|
|
}
|
|
|
|
func resourceDiff(res1, res2 types.Resource) string {
|
|
return cmp.Diff(res1, res2,
|
|
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
|
|
cmpopts.EquateEmpty())
|
|
}
|
|
|
|
func newHealthCheckConfig(t *testing.T, opts ...func(*healthcheckconfigv1.HealthCheckConfig)) types.Resource {
|
|
r := resourceFromYAML(t, healthCheckConfigYAML)
|
|
inner := r.(types.Resource153UnwrapperT[*healthcheckconfigv1.HealthCheckConfig]).UnwrapT()
|
|
for _, opt := range opts {
|
|
opt(inner)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// TestSyncUpgadeWindowStartHour verifies the core logic of the upgrade window start
|
|
// hour behavior.
|
|
func TestSyncUpgradeWindowStartHour(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
conf := setupConfig(t)
|
|
authServer, err := auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { authServer.Close() })
|
|
|
|
// no getter is registered, sync should fail
|
|
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
// maintenance config does not exist yet
|
|
cmc, err := authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.Error(t, err)
|
|
require.True(t, trace.IsNotFound(err))
|
|
require.Nil(t, cmc)
|
|
|
|
// set up fake getter
|
|
var mu sync.Mutex
|
|
var fakeHour int64
|
|
var fakeError error
|
|
authServer.SetUpgradeWindowStartHourGetter(func(ctx context.Context) (int64, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return fakeHour, fakeError
|
|
})
|
|
|
|
// sync should now succeed
|
|
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok := cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
require.Equal(t, uint32(0), agentWindow.UTCStartHour)
|
|
require.Equal(t, []string{"Mon", "Tue", "Wed", "Thu"}, agentWindow.Weekdays)
|
|
|
|
// change the served hour
|
|
mu.Lock()
|
|
fakeHour = 16
|
|
mu.Unlock()
|
|
|
|
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok = cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
require.Equal(t, uint32(16), agentWindow.UTCStartHour)
|
|
|
|
// set sync to fail with out of range hour
|
|
mu.Lock()
|
|
fakeHour = 36
|
|
mu.Unlock()
|
|
|
|
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok = cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
// verify that the old hour value persists since the sync failed
|
|
require.Equal(t, uint32(16), agentWindow.UTCStartHour)
|
|
|
|
// set sync to fail with impossible int type-cast
|
|
mu.Lock()
|
|
fakeHour = math.MaxInt64
|
|
mu.Unlock()
|
|
|
|
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok = cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
// verify that the old hour value persists since the sync failed
|
|
require.Equal(t, uint32(16), agentWindow.UTCStartHour)
|
|
|
|
mu.Lock()
|
|
fakeHour = 18
|
|
mu.Unlock()
|
|
|
|
// sync should now succeed again
|
|
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok = cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
// verify that we got the new hour value
|
|
require.Equal(t, uint32(18), agentWindow.UTCStartHour)
|
|
|
|
// set sync to fail with error
|
|
mu.Lock()
|
|
fakeHour = 12
|
|
fakeError = fmt.Errorf("uh-oh")
|
|
mu.Unlock()
|
|
|
|
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok = cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
// verify that the old hour value persists since the sync failed
|
|
require.Equal(t, uint32(18), agentWindow.UTCStartHour)
|
|
|
|
// recover and set hour to zero
|
|
mu.Lock()
|
|
fakeHour = 0
|
|
fakeError = nil
|
|
mu.Unlock()
|
|
|
|
// sync should now succeed again
|
|
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
|
|
|
|
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
|
|
require.NoError(t, err)
|
|
|
|
agentWindow, ok = cmc.GetAgentUpgradeWindow()
|
|
require.True(t, ok)
|
|
|
|
// verify that we got the new hour value
|
|
require.Equal(t, uint32(0), agentWindow.UTCStartHour)
|
|
}
|
|
|
|
// TestIdentityChecker verifies auth identity properly validates host
|
|
// certificates when connecting to an SSH server.
|
|
func TestIdentityChecker(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
conf := setupConfig(t)
|
|
authServer, err := auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { authServer.Close() })
|
|
|
|
lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{
|
|
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
|
Component: teleport.ComponentAuth,
|
|
Client: authServer,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
authServer.SetLockWatcher(lockWatcher)
|
|
|
|
clusterName, err := authServer.GetDomainName()
|
|
require.NoError(t, err)
|
|
|
|
ca, err := authServer.GetCertAuthority(ctx, types.CertAuthID{
|
|
Type: types.HostCA,
|
|
DomainName: clusterName,
|
|
}, true)
|
|
require.NoError(t, err)
|
|
|
|
signers, err := sshutils.GetSigners(ca)
|
|
require.NoError(t, err)
|
|
require.Len(t, signers, 1)
|
|
|
|
realCert, err := apisshutils.MakeRealHostCert(signers[0])
|
|
require.NoError(t, err)
|
|
|
|
spoofedCert, err := apisshutils.MakeSpoofedHostCert(signers[0])
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
desc string
|
|
cert ssh.Signer
|
|
err bool
|
|
}{
|
|
{
|
|
desc: "should be able to connect with real cert",
|
|
cert: realCert,
|
|
err: false,
|
|
},
|
|
{
|
|
desc: "should not be able to connect with spoofed cert",
|
|
cert: spoofedCert,
|
|
err: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
handler := sshutils.NewChanHandlerFunc(func(_ context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) {
|
|
ch, _, err := nch.Accept()
|
|
require.NoError(t, err)
|
|
require.NoError(t, ch.Close())
|
|
})
|
|
sshServer, err := sshutils.NewServer(
|
|
"test",
|
|
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
|
|
handler,
|
|
sshutils.StaticHostSigners(test.cert),
|
|
sshutils.AuthMethods{NoClient: true},
|
|
sshutils.SetInsecureSkipHostValidation(),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { sshServer.Close() })
|
|
require.NoError(t, sshServer.Start())
|
|
|
|
identity, err := auth.GenerateIdentity(authServer, state.IdentityID{
|
|
Role: types.RoleNode,
|
|
HostUUID: uuid.New().String(),
|
|
NodeName: "node-1",
|
|
}, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
sshClientConfig, err := identity.SSHClientConfig(false)
|
|
require.NoError(t, err)
|
|
|
|
dialer := proxy.DialerFromEnvironment(sshServer.Addr())
|
|
sconn, err := dialer.Dial(ctx, "tcp", sshServer.Addr(), sshClientConfig)
|
|
if test.err {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NoError(t, sconn.Close())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitCreatesCertsIfMissing(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
conf := setupConfig(t)
|
|
auth, err := auth.Init(ctx, conf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = auth.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
for _, caType := range types.CertAuthTypes {
|
|
cert, err := auth.GetCertAuthorities(ctx, caType, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, cert, 1)
|
|
}
|
|
}
|
|
|
|
func TestTeleportProcessAuthVersionUpgradeCheck(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialVersion string
|
|
initialProcVersion string
|
|
expectedVersion string
|
|
expectError bool
|
|
skipCheck bool
|
|
}{
|
|
{
|
|
name: "first-launch",
|
|
initialVersion: "",
|
|
expectedVersion: teleport.Version,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "old-version-upgrade",
|
|
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-1),
|
|
expectedVersion: teleport.Version,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "old-version-upgrade-proc",
|
|
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-1),
|
|
expectedVersion: teleport.Version,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "major-upgrade-fail",
|
|
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
|
|
expectedVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "major-upgrade-fail-proc",
|
|
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "major-upgrade-fail-proc-with-skip-check",
|
|
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
|
|
expectedVersion: teleport.Version,
|
|
skipCheck: true,
|
|
},
|
|
{
|
|
name: "major-upgrade-with-dev-skip-check",
|
|
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
|
|
expectedVersion: teleport.Version,
|
|
expectError: false,
|
|
skipCheck: true,
|
|
},
|
|
{
|
|
name: "major-downgrade-fail",
|
|
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
|
|
expectedVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "major-downgrade-fail-proc",
|
|
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "major-downgrade-fail-proc-with-skip-check",
|
|
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
|
|
expectedVersion: teleport.Version,
|
|
skipCheck: true,
|
|
},
|
|
{
|
|
name: "major-downgrade-with-dev-skip-check",
|
|
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
|
|
expectedVersion: teleport.Version,
|
|
expectError: false,
|
|
skipCheck: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
authCfg := setupConfig(t)
|
|
authCfg.InsecureMode = true
|
|
service, err := local.NewBackendInfoService(authCfg.Backend)
|
|
require.NoError(t, err)
|
|
|
|
if test.initialProcVersion != "" {
|
|
ver, err := semver.NewVersion(test.initialProcVersion)
|
|
require.NoError(t, err)
|
|
err = authCfg.VersionStorage.WriteTeleportVersion(ctx, *ver)
|
|
require.NoError(t, err)
|
|
}
|
|
if test.initialVersion != "" {
|
|
ver, err := semver.NewVersion(test.initialVersion)
|
|
require.NoError(t, err)
|
|
backendInfo, err := backendinfo.NewBackendInfo(backendinfov1.BackendInfoSpec_builder{
|
|
TeleportVersion: ver.String(),
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
_, err = service.CreateBackendInfo(ctx, backendInfo)
|
|
require.NoError(t, err)
|
|
}
|
|
if test.skipCheck {
|
|
authCfg.SkipVersionCheck = true
|
|
}
|
|
|
|
_, err = auth.Init(ctx, authCfg)
|
|
if test.expectError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
// Verifies that version is removed from process storage.
|
|
if test.initialProcVersion != "" && !test.expectError {
|
|
_, err := authCfg.VersionStorage.GetTeleportVersion(ctx)
|
|
require.True(t, trace.IsNotFound(err))
|
|
}
|
|
|
|
if test.expectedVersion != "" {
|
|
backendInfo, err := service.GetBackendInfo(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.expectedVersion, backendInfo.GetSpec().GetTeleportVersion())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockDatabaseObjectImportRules struct {
|
|
services.DatabaseObjectImportRules
|
|
listRules []*dbobjectimportrulev1.DatabaseObjectImportRule
|
|
created *dbobjectimportrulev1.DatabaseObjectImportRule
|
|
upserted *dbobjectimportrulev1.DatabaseObjectImportRule
|
|
}
|
|
|
|
func (m *mockDatabaseObjectImportRules) ListDatabaseObjectImportRules(context.Context, int, string) ([]*dbobjectimportrulev1.DatabaseObjectImportRule, string, error) {
|
|
return m.listRules, "", nil
|
|
}
|
|
func (m *mockDatabaseObjectImportRules) CreateDatabaseObjectImportRule(ctx context.Context, rule *dbobjectimportrulev1.DatabaseObjectImportRule) (*dbobjectimportrulev1.DatabaseObjectImportRule, error) {
|
|
m.created = rule
|
|
return rule, nil
|
|
}
|
|
func (m *mockDatabaseObjectImportRules) UpsertDatabaseObjectImportRule(ctx context.Context, rule *dbobjectimportrulev1.DatabaseObjectImportRule) (*dbobjectimportrulev1.DatabaseObjectImportRule, error) {
|
|
m.upserted = rule
|
|
return rule, nil
|
|
}
|
|
|
|
func Test_createPresetDatabaseObjectImportRule(t *testing.T) {
|
|
presetRule := databaseobjectimportrule.NewPresetImportAllObjectsRule()
|
|
require.NotNil(t, presetRule)
|
|
|
|
customRule, err := databaseobjectimportrule.NewDatabaseObjectImportRule("dev_rule", dbobjectimportrulev1.DatabaseObjectImportRuleSpec_builder{
|
|
Priority: 100,
|
|
DatabaseLabels: label.FromMap(map[string][]string{"env": {"dev"}}),
|
|
Mappings: []*dbobjectimportrulev1.DatabaseObjectImportRuleMapping{dbobjectimportrulev1.DatabaseObjectImportRuleMapping_builder{
|
|
Match: dbobjectimportrulev1.DatabaseObjectImportMatch_builder{
|
|
TableNames: []string{"*"},
|
|
}.Build(),
|
|
AddLabels: map[string]string{
|
|
"env": "dev",
|
|
},
|
|
Scope: dbobjectimportrulev1.DatabaseObjectImportScope_builder{
|
|
SchemaNames: []string{"public"},
|
|
}.Build(),
|
|
}.Build()},
|
|
}.Build())
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
existingRules []*dbobjectimportrulev1.DatabaseObjectImportRule
|
|
expectCreate *dbobjectimportrulev1.DatabaseObjectImportRule
|
|
expectUpsert *dbobjectimportrulev1.DatabaseObjectImportRule
|
|
}{
|
|
{
|
|
name: "create preset in new cluster",
|
|
expectCreate: presetRule,
|
|
},
|
|
{
|
|
name: "no action with custom rule",
|
|
existingRules: []*dbobjectimportrulev1.DatabaseObjectImportRule{customRule},
|
|
},
|
|
{
|
|
name: "no action with preset rule",
|
|
existingRules: []*dbobjectimportrulev1.DatabaseObjectImportRule{presetRule},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
m := &mockDatabaseObjectImportRules{
|
|
listRules: test.existingRules,
|
|
}
|
|
|
|
err := auth.CreatePresetDatabaseObjectImportRule(context.Background(), m)
|
|
require.NoError(t, err)
|
|
require.True(t, proto.Equal(test.expectCreate, m.created))
|
|
require.True(t, proto.Equal(test.expectUpsert, m.upserted))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInitWithAutoUpdateResources verifies that auth init support bootstrapping and apply
|
|
// `AutoUpdateConfig` and `AutoUpdateVersion` resources as well as unmarshalling them from
|
|
// yaml configuration.
|
|
func TestInitWithAutoUpdateResources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const autoUpdateConfigYAML = `kind: autoupdate_config
|
|
metadata:
|
|
name: autoupdate-config
|
|
spec:
|
|
tools:
|
|
mode: enabled
|
|
version: v1`
|
|
const autoUpdateVersionYAML = `kind: autoupdate_version
|
|
metadata:
|
|
name: autoupdate-version
|
|
spec:
|
|
tools:
|
|
target_version: 1.2.3
|
|
version: v1`
|
|
|
|
ctx := context.Background()
|
|
resources := []types.Resource{
|
|
resourceFromYAML(t, autoUpdateConfigYAML),
|
|
resourceFromYAML(t, autoUpdateVersionYAML),
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
fn func(cfg *auth.InitConfig)
|
|
}{
|
|
{name: "bootstrap", fn: func(cfg *auth.InitConfig) { cfg.BootstrapResources = resources }},
|
|
{name: "apply", fn: func(cfg *auth.InitConfig) { cfg.ApplyOnStartupResources = resources }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := setupConfig(t)
|
|
test.fn(&cfg)
|
|
auth, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
config, err := auth.GetAutoUpdateConfig(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "enabled", config.GetSpec().GetTools().GetMode())
|
|
|
|
version, err := auth.GetAutoUpdateVersion(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "1.2.3", version.GetSpec().GetTools().GetTargetVersion())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitWithWorkloadIdentityResources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := t.Context()
|
|
resources := []types.Resource{
|
|
resourceFromYAML(t, workloadIdentityYAML),
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
fn func(cfg *auth.InitConfig)
|
|
}{
|
|
{name: "bootstrap", fn: func(cfg *auth.InitConfig) { cfg.BootstrapResources = resources }},
|
|
{name: "apply", fn: func(cfg *auth.InitConfig) { cfg.ApplyOnStartupResources = resources }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cfg := setupConfig(t)
|
|
test.fn(&cfg)
|
|
auth, err := auth.Init(ctx, cfg)
|
|
require.NoError(t, err)
|
|
|
|
workloadIdentity, err := auth.GetWorkloadIdentity(ctx, workloadidentityv1pb.GetWorkloadIdentityRequest_builder{Name: "example-workload-identity"}.Build())
|
|
require.NoError(t, err)
|
|
require.Equal(t, "/svc/example", workloadIdentity.GetSpec().GetSpiffe().GetId())
|
|
})
|
|
}
|
|
}
|