Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support specifying assume_role_arn for Kube cluster matchers #28282

Merged
merged 2 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1323,12 +1323,13 @@ func applyKubeConfig(fc *FileConfig, cfg *servicecfg.Config) error {
}

for _, matcher := range fc.Kube.ResourceMatchers {
if matcher.AWS.AssumeRoleARN != "" {
return trace.NotImplemented("assume_role_arn is not supported for kube resource matchers")
}
cfg.Kube.ResourceMatchers = append(cfg.Kube.ResourceMatchers,
services.ResourceMatcher{
Labels: matcher.Labels,
AWS: services.ResourceMatcherAWS{
AssumeRoleARN: matcher.AWS.AssumeRoleARN,
ExternalID: matcher.AWS.ExternalID,
},
})
}

Expand Down
21 changes: 19 additions & 2 deletions lib/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3439,7 +3439,7 @@ func TestApplyConfig_JamfService(t *testing.T) {
const password = "supersecret!!1!"
passwordFile := filepath.Join(tempDir, "test_jamf_password.txt")
require.NoError(t,
os.WriteFile(passwordFile, []byte(password+"\n"), 0400),
os.WriteFile(passwordFile, []byte(password+"\n"), 0o400),
"WriteFile(%q) failed", passwordFile)

minimalYAML := fmt.Sprintf(`
Expand Down Expand Up @@ -4026,7 +4026,24 @@ func TestApplyKubeConfig(t *testing.T) {
},
},
},
wantError: true,
wantError: false,
wantServiceConfig: servicecfg.KubeConfig{
ListenAddr: utils.MustParseAddr("0.0.0.0:8888"),
KubeconfigPath: "path-to-kubeconfig",
ResourceMatchers: []services.ResourceMatcher{
{
Labels: map[string]apiutils.Strings{"a": {"b"}},
AWS: services.ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/KubeAccess",
ExternalID: "externalID123",
},
},
},
Limiter: limiter.Config{
MaxConnections: defaults.LimiterMaxConnections,
MaxNumberOfUsers: 250,
},
},
},
{
name: "valid",
Expand Down
81 changes: 60 additions & 21 deletions lib/kube/proxy/cluster_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/gravitational/teleport/lib/cloud/gcp"
"github.com/gravitational/teleport/lib/labels"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
)

// kubeDetails contain the cluster-related details including authentication.
Expand All @@ -46,21 +47,36 @@ type kubeDetails struct {
kubeCluster types.KubeCluster
}

// clusterDetailsConfig contains the configuration for creating a proxied cluster.
type clusterDetailsConfig struct {
// cloudClients is the cloud clients to use for dynamic clusters.
cloudClients cloud.Clients
// cluster is the cluster to create a proxied cluster for.
cluster types.KubeCluster
// log is the logger to use.
log *logrus.Entry
// checker is the permissions checker to use.
checker servicecfg.ImpersonationPermissionsChecker
// resourceMatchers is the list of resource matchers to match the cluster against
// to determine if we should assume the role or not for AWS.
resourceMatchers []services.ResourceMatcher
}

// newClusterDetails creates a proxied kubeDetails structure given a dynamic cluster.
func newClusterDetails(ctx context.Context, cloudClients cloud.Clients, cluster types.KubeCluster, log *logrus.Entry, checker servicecfg.ImpersonationPermissionsChecker) (*kubeDetails, error) {
func newClusterDetails(ctx context.Context, cfg clusterDetailsConfig) (*kubeDetails, error) {
var dynLabels *labels.Dynamic

creds, err := getKubeClusterCredentials(ctx, cloudClients, cluster, log, checker)
creds, err := getKubeClusterCredentials(ctx, cfg)
if err != nil {
return nil, trace.Wrap(err)
}

if len(cluster.GetDynamicLabels()) > 0 {
if len(cfg.cluster.GetDynamicLabels()) > 0 {
dynLabels, err = labels.NewDynamic(
ctx,
&labels.DynamicConfig{
Labels: cluster.GetDynamicLabels(),
Log: log,
Labels: cfg.cluster.GetDynamicLabels(),
Log: cfg.log,
})
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -72,7 +88,7 @@ func newClusterDetails(ctx context.Context, cloudClients cloud.Clients, cluster
return &kubeDetails{
kubeCreds: creds,
dynamicLabels: dynLabels,
kubeCluster: cluster,
kubeCluster: cfg.cluster,
}, nil
}

Expand All @@ -85,18 +101,19 @@ func (k *kubeDetails) Close() {
}

// getKubeClusterCredentials generates kube credentials for dynamic clusters.
func getKubeClusterCredentials(ctx context.Context, cloudClients cloud.Clients, cluster types.KubeCluster, log *logrus.Entry, checker servicecfg.ImpersonationPermissionsChecker) (kubeCreds, error) {
func getKubeClusterCredentials(ctx context.Context, cfg clusterDetailsConfig) (kubeCreds, error) {
dynCredsCfg := dynamicCredsConfig{kubeCluster: cfg.cluster, log: cfg.log, checker: cfg.checker, resourceMatchers: cfg.resourceMatchers}
switch {
case cluster.IsKubeconfig():
return getStaticCredentialsFromKubeconfig(ctx, cluster, log, checker)
case cluster.IsAzure():
return getAzureCredentials(ctx, cloudClients, dynamicCredsConfig{kubeCluster: cluster, log: log, checker: checker})
case cluster.IsAWS():
return getAWSCredentials(ctx, cloudClients, dynamicCredsConfig{kubeCluster: cluster, log: log, checker: checker})
case cluster.IsGCP():
return getGCPCredentials(ctx, cloudClients, dynamicCredsConfig{kubeCluster: cluster, log: log, checker: checker})
case cfg.cluster.IsKubeconfig():
return getStaticCredentialsFromKubeconfig(ctx, cfg.cluster, cfg.log, cfg.checker)
case cfg.cluster.IsAzure():
return getAzureCredentials(ctx, cfg.cloudClients, dynCredsCfg)
case cfg.cluster.IsAWS():
return getAWSCredentials(ctx, cfg.cloudClients, dynCredsCfg)
case cfg.cluster.IsGCP():
return getGCPCredentials(ctx, cfg.cloudClients, dynCredsCfg)
default:
return nil, trace.BadParameter("authentication method provided for cluster %q not supported", cluster.GetName())
return nil, trace.BadParameter("authentication method provided for cluster %q not supported", cfg.cluster.GetName())
}
}

Expand Down Expand Up @@ -132,17 +149,39 @@ func azureRestConfigClient(cloudClients cloud.Clients) dynamicCredsClient {
// getAWSCredentials creates a dynamicKubeCreds that generates and updates the access credentials to a EKS kubernetes cluster.
func getAWSCredentials(ctx context.Context, cloudClients cloud.Clients, cfg dynamicCredsConfig) (*dynamicKubeCreds, error) {
// create a client that returns the credentials for kubeCluster
cfg.client = getAWSClientRestConfig(cloudClients, cfg.clock)
cfg.client = getAWSClientRestConfig(cloudClients, cfg.clock, cfg.resourceMatchers)
creds, err := newDynamicKubeCreds(ctx, cfg)
return creds, trace.Wrap(err)
}

// getAWSResourceMatcherToCluster returns the AWS assume role ARN and external ID for the cluster that matches the kubeCluster.
// If no match is found, nil is returned, which means that we should not attempt to assume a role.
func getAWSResourceMatcherToCluster(kubeCluster types.KubeCluster, resourceMatchers []services.ResourceMatcher) *services.ResourceMatcherAWS {
if !kubeCluster.IsAWS() {
return nil
}
for _, matcher := range resourceMatchers {
if len(matcher.Labels) == 0 || matcher.AWS.AssumeRoleARN == "" {
continue
}
if match, _, _ := services.MatchLabels(matcher.Labels, kubeCluster.GetAllLabels()); !match {
continue
}

return &(matcher.AWS)
}
return nil
}

// getAWSClientRestConfig creates a dynamicCredsClient that generates returns credentials to EKS clusters.
func getAWSClientRestConfig(cloudClients cloud.Clients, clock clockwork.Clock) dynamicCredsClient {
func getAWSClientRestConfig(cloudClients cloud.Clients, clock clockwork.Clock, resourceMatchers []services.ResourceMatcher) dynamicCredsClient {
return func(ctx context.Context, cluster types.KubeCluster) (*rest.Config, time.Time, error) {
// TODO(gavin): support assume_role_arn for AWS EKS.
region := cluster.GetAWSConfig().Region
regionalClient, err := cloudClients.GetAWSEKSClient(ctx, region)
var opts []cloud.AWSAssumeRoleOptionFn
if awsAssume := getAWSResourceMatcherToCluster(cluster, resourceMatchers); awsAssume != nil {
opts = append(opts, cloud.WithAssumeRole(awsAssume.AssumeRoleARN, awsAssume.ExternalID))
}
regionalClient, err := cloudClients.GetAWSEKSClient(ctx, region, opts...)
if err != nil {
return nil, time.Time{}, trace.Wrap(err)
}
Expand All @@ -164,7 +203,7 @@ func getAWSClientRestConfig(cloudClients cloud.Clients, clock clockwork.Clock) d
return nil, time.Time{}, trace.BadParameter("invalid api endpoint for cluster %q", cluster.GetAWSConfig().Name)
}

stsClient, err := cloudClients.GetAWSSTSClient(ctx, region)
stsClient, err := cloudClients.GetAWSSTSClient(ctx, region, opts...)
if err != nil {
return nil, time.Time{}, trace.Wrap(err)
}
Expand Down
2 changes: 2 additions & 0 deletions lib/kube/proxy/kube_creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
)

type kubeCreds interface {
Expand Down Expand Up @@ -171,6 +172,7 @@ type dynamicCredsConfig struct {
checker servicecfg.ImpersonationPermissionsChecker
clock clockwork.Clock
initialRenewInterval time.Duration
resourceMatchers []services.ResourceMatcher
}

func (d *dynamicCredsConfig) checkAndSetDefaults() error {
Expand Down
96 changes: 84 additions & 12 deletions lib/kube/proxy/kube_creds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ import (
"k8s.io/client-go/rest"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/cloud/azure"
"github.com/gravitational/teleport/lib/cloud/gcp"
"github.com/gravitational/teleport/lib/cloud/mocks"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/services"
)

// Test_DynamicKubeCreds tests the dynamic kube credrentials generator for
Expand Down Expand Up @@ -102,13 +104,14 @@ func Test_DynamicKubeCreds(t *testing.T) {
Host: "sts.amazonaws.com",
Path: "/?Action=GetCallerIdentity&Version=2011-06-15",
}
sts := &mocks.STSMock{
// u is used to presign the request
// here we just verify the pre-signed request includes this url.
URL: u,
}
// mock clients
cloudclients := &cloud.TestCloudClients{
STS: &mocks.STSMock{
// u is used to presign the request
// here we just verify the pre-signed request includes this url.
URL: u,
},
STS: sts,
EKS: &mocks.EKSMock{
Notify: notify,
Clusters: []*eks.Cluster{
Expand Down Expand Up @@ -164,22 +167,85 @@ func Test_DynamicKubeCreds(t *testing.T) {
},
},
}

validateEKSToken := func(token string) error {
if token == "" {
return trace.BadParameter("missing bearer token")
}
tokens := strings.Split(token, ".")
if len(tokens) != 2 {
return trace.BadParameter("invalid bearer token")
}
if tokens[0] != "k8s-aws-v1" {
return trace.BadParameter("token must start with k8s-aws-v1")
}
dec, err := base64.RawStdEncoding.DecodeString(tokens[1])
if err != nil {
return trace.Wrap(err)
}
if string(dec) != u.String() {
return trace.BadParameter("invalid token payload")
}
return nil
}
type args struct {
cluster types.KubeCluster
client dynamicCredsClient
validateBearerToken func(string) error
}
tests := []struct {
name string
args args
wantAddr string
name string
args args
wantAddr string
wantAssumedRole []string
wantExternalIds []string
}{
{
name: "aws eks cluster",
name: "aws eks cluster without assume role",
args: args{
cluster: awsKube,
client: getAWSClientRestConfig(cloudclients, fakeClock, nil),
validateBearerToken: validateEKSToken,
},
wantAddr: "api.eks.us-west-2.amazonaws.com:443",
},
{
name: "aws eks cluster with unmatched assume role",
args: args{
cluster: awsKube,
client: getAWSClientRestConfig(cloudclients, fakeClock),
client: getAWSClientRestConfig(cloudclients, fakeClock, []services.ResourceMatcher{
{
Labels: types.Labels{
"rand": []string{"value"},
},
AWS: services.ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/eks-role",
ExternalID: "1234567890",
},
},
}),
validateBearerToken: validateEKSToken,
},
wantAddr: "api.eks.us-west-2.amazonaws.com:443",
},
{
name: "aws eks cluster with assume role",
args: args{
cluster: awsKube,
client: getAWSClientRestConfig(
cloudclients,
fakeClock,
[]services.ResourceMatcher{
{
Labels: types.Labels{
types.Wildcard: []string{types.Wildcard},
},
AWS: services.ResourceMatcherAWS{
AssumeRoleARN: "arn:aws:iam::123456789012:role/eks-role",
ExternalID: "1234567890",
},
},
},
),
validateBearerToken: func(token string) error {
if token == "" {
return trace.BadParameter("missing bearer token")
Expand All @@ -201,7 +267,9 @@ func Test_DynamicKubeCreds(t *testing.T) {
return nil
},
},
wantAddr: "api.eks.us-west-2.amazonaws.com:443",
wantAddr: "api.eks.us-west-2.amazonaws.com:443",
wantAssumedRole: []string{"arn:aws:iam::123456789012:role/eks-role"},
wantExternalIds: []string{"1234567890"},
},
{
name: "gcp gke cluster",
Expand Down Expand Up @@ -261,6 +329,10 @@ func Test_DynamicKubeCreds(t *testing.T) {
}
}
require.NoError(t, got.close())

require.Equal(t, tt.wantAssumedRole, utils.Deduplicate(sts.GetAssumedRoleARNs()))
require.Equal(t, tt.wantExternalIds, utils.Deduplicate(sts.GetAssumedRoleExternalIDs()))
sts.ResetAssumeRoleHistory()
})
}
}
22 changes: 14 additions & 8 deletions lib/kube/proxy/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,13 @@ func (m *monitoredKubeClusters) get() types.ResourcesWithLabelsMap {
func (s *TLSServer) registerKubeCluster(ctx context.Context, cluster types.KubeCluster) error {
clusterDetails, err := newClusterDetails(
ctx,
s.CloudClients,
cluster,
s.log,
s.CheckImpersonationPermissions,
clusterDetailsConfig{
cloudClients: s.CloudClients,
cluster: cluster,
log: s.log,
checker: s.CheckImpersonationPermissions,
resourceMatchers: s.ResourceMatchers,
},
)
if err != nil {
return trace.Wrap(err)
Expand All @@ -196,10 +199,13 @@ func (s *TLSServer) registerKubeCluster(ctx context.Context, cluster types.KubeC
func (s *TLSServer) updateKubeCluster(ctx context.Context, cluster types.KubeCluster) error {
clusterDetails, err := newClusterDetails(
ctx,
s.CloudClients,
cluster,
s.log,
s.CheckImpersonationPermissions,
clusterDetailsConfig{
cloudClients: s.CloudClients,
cluster: cluster,
log: s.log,
checker: s.CheckImpersonationPermissions,
resourceMatchers: s.ResourceMatchers,
},
)
if err != nil {
return trace.Wrap(err)
Expand Down