Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6780,6 +6780,10 @@ message AWSMatcher {
// KubeAppDiscovery controls whether Kubernetes App Discovery will be enabled for agents running on
// discovered clusters, currently only affects AWS EKS discovery in integration mode.
bool KubeAppDiscovery = 8 [(gogoproto.jsontag) = "kube_app_discovery,omitempty"];
// SetupAccessForARN is the role that the discovery service should create EKS Access Entries for.
// This value should match the IAM identity that Teleport Kubernetes Service uses.
// If this value is empty, the discovery service will attempt to set up access for its own identity (self).
string SetupAccessForARN = 9 [(gogoproto.jsontag) = "setup_access_for_arn,omitempty"];
}

// AssumeRole provides a role ARN and ExternalID to assume an AWS role
Expand Down
3 changes: 2 additions & 1 deletion api/types/discoveryconfig/derived.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions api/types/matchers_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ func (m *AWSMatcher) CheckAndSetDefaults() error {
}
}

if m.SetupAccessForARN != "" {
if !slices.Contains(m.Types, AWSMatcherEKS) {
return trace.BadParameter("discovery service AWS matcher setup_access_for_arn is only supported for eks")
}
if err := awsapiutils.CheckRoleARN(m.SetupAccessForARN); err != nil {
return trace.BadParameter("invalid setup access for ARN: %v", err)
}
}

if m.Tags == nil || len(m.Tags) == 0 {
m.Tags = map[string]apiutils.Strings{Wildcard: {Wildcard}}
}
Expand Down
345 changes: 197 additions & 148 deletions api/types/types.pb.go

Large diffs are not rendered by default.

55 changes: 6 additions & 49 deletions lib/cloud/azure/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import (
authztypes "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

"github.com/gravitational/teleport/lib/fixtures"
)

// AKSAuthMethod defines the authentication method for AKS cluster.
Expand Down Expand Up @@ -348,7 +350,7 @@ func (c *aksClient) getAzureADCredentials(ctx context.Context, cluster ClusterCr
adminCredentialsErr = trace.WrapWithMessage(adminCredentialsErr, `Tried to grant access to %s/%s using aks.ListClusterAdminCredentials`, cluster.ResourceGroup, cluster.ResourceName)
// if the creation failed, then the agent will try to run a command to create them.
fallthrough
case err != nil:
default:
if runCMDErr = c.grantAccessWithCommand(ctx, cluster.ResourceGroup, cluster.ResourceName, cluster.TenantID, groupID); runCMDErr != nil {
return nil, time.Time{}, trace.Wrap(err)
}
Expand All @@ -360,7 +362,6 @@ func (c *aksClient) getAzureADCredentials(ctx context.Context, cluster ClusterCr
return nil, time.Time{}, trace.WrapWithMessage(trace.NewAggregate(adminCredentialsErr, runCMDErr), `Cannot grant access to %s/%s AKS cluster`, cluster.ResourceGroup, cluster.ResourceName)
}

return nil, time.Time{}, trace.NotImplemented("code shouldn't reach")
}

// getAdminCredentials returns the cluster admin credentials by calling ListClusterAdminCredentials method.
Expand Down Expand Up @@ -483,7 +484,7 @@ func (c *aksClient) grantAccessWithAdminCredentials(ctx context.Context, adminCf
func (c *aksClient) upsertClusterRoleWithAdminCredentials(ctx context.Context, client *kubernetes.Clientset) error {
clusterRole := &v1.ClusterRole{}

if err := yaml.Unmarshal([]byte(clusterRoleTemplate), clusterRole); err != nil {
if err := yaml.Unmarshal([]byte(fixtures.KubeClusterRoleTemplate), clusterRole); err != nil {
return trace.Wrap(err)
}

Expand All @@ -509,7 +510,7 @@ func (c *aksClient) upsertClusterRoleWithAdminCredentials(ctx context.Context, c
func (c *aksClient) upsertClusterRoleBindingWithAdminCredentials(ctx context.Context, client *kubernetes.Clientset, groupID string) error {
clusterRoleBinding := &v1.ClusterRoleBinding{}

if err := yaml.Unmarshal([]byte(clusterRoleBindingTemplate), clusterRoleBinding); err != nil {
if err := yaml.Unmarshal([]byte(fixtures.KubeClusterRoleBindingTemplate), clusterRoleBinding); err != nil {
return trace.Wrap(err)
}

Expand Down Expand Up @@ -664,50 +665,6 @@ func isAKSClusterRunning(properties *armcontainerservice.ManagedClusterPropertie
return false
}

const (
clusterRoleTemplate = `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: teleport
rules:
- apiGroups:
- ""
resources:
- users
- groups
- serviceaccounts
verbs:
- impersonate
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- "authorization.k8s.io"
resources:
- selfsubjectaccessreviews
- selfsubjectrulesreviews
verbs:
- create
`
clusterRoleBindingTemplate = `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: teleport
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: teleport
subjects:
- kind: Group
name: group_name
apiGroup: rbac.authorization.k8s.io`
)

// kubectlApplyString generates a kubectl apply command to create the ClusterRole
// and ClusterRoleBinding.
// cat <<EOF | kubectl apply -f -
Expand Down Expand Up @@ -766,5 +723,5 @@ func kubectlApplyString(group string) string {
%s
---
%s
EOF`, clusterRoleTemplate, strings.ReplaceAll(clusterRoleBindingTemplate, "group_name", group))
EOF`, fixtures.KubeClusterRoleTemplate, strings.ReplaceAll(fixtures.KubeClusterRoleBindingTemplate, "group_name", group))
}
17 changes: 9 additions & 8 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -1611,14 +1611,15 @@ func applyDiscoveryConfig(fc *FileConfig, cfg *servicecfg.Config) error {
}

serviceMatcher := types.AWSMatcher{
Types: matcher.Types,
Regions: matcher.Regions,
AssumeRole: assumeRole,
Tags: matcher.Tags,
Params: installParams,
SSM: &types.AWSSSM{DocumentName: matcher.SSM.DocumentName},
Integration: matcher.Integration,
KubeAppDiscovery: matcher.KubeAppDiscovery,
Types: matcher.Types,
Regions: matcher.Regions,
AssumeRole: assumeRole,
Tags: matcher.Tags,
Params: installParams,
SSM: &types.AWSSSM{DocumentName: matcher.SSM.DocumentName},
Integration: matcher.Integration,
KubeAppDiscovery: matcher.KubeAppDiscovery,
SetupAccessForARN: matcher.SetupAccessForARN,
}
if err := serviceMatcher.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
Expand Down
2 changes: 2 additions & 0 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,8 @@ type AWSMatcher struct {
// KubeAppDiscovery controls whether Kubernetes App Discovery will be enabled for agents running on
// discovered clusters, currently only affects AWS EKS discovery in integration mode.
KubeAppDiscovery bool `yaml:"kube_app_discovery"`
// SetupAccessForARN is the role that the discovery service should create EKS Access Entries for.
SetupAccessForARN string `yaml:"setup_access_for_arn"`
}

// InstallParams sets join method to use on discovered nodes
Expand Down
66 changes: 66 additions & 0 deletions lib/fixtures/kube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Teleport
* Copyright (C) 2024 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 fixtures

// KubeClusterRoleTemplate is a template for a Kubernetes ClusterRole
// that Teleport uses to access Kubernetes resources.
const KubeClusterRoleTemplate = `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: teleport
rules:
- apiGroups:
- ""
resources:
- users
- groups
- serviceaccounts
verbs:
- impersonate
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- "authorization.k8s.io"
resources:
- selfsubjectaccessreviews
- selfsubjectrulesreviews
verbs:
- create
`

// KubeClusterRoleBindingTemplate is a template for a Kubernetes ClusterRoleBinding
// that Teleport uses to access Kubernetes resources.
const KubeClusterRoleBindingTemplate = `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: teleport
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: teleport
subjects:
- kind: Group
name: group_name
apiGroup: rbac.authorization.k8s.io`
38 changes: 2 additions & 36 deletions lib/kube/proxy/cluster_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/eks"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
Expand All @@ -42,6 +40,7 @@ import (
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/cloud/azure"
"github.com/gravitational/teleport/lib/cloud/gcp"
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
"github.com/gravitational/teleport/lib/labels"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
Expand Down Expand Up @@ -343,7 +342,7 @@ func getAWSClientRestConfig(cloudClients cloud.Clients, clock clockwork.Clock, r
return nil, time.Time{}, trace.Wrap(err)
}

token, exp, err := genAWSToken(stsClient, cluster.GetAWSConfig().Name, clock)
token, exp, err := kubeutils.GenAWSEKSToken(stsClient, cluster.GetAWSConfig().Name, clock)
if err != nil {
return nil, time.Time{}, trace.Wrap(err)
}
Expand All @@ -358,39 +357,6 @@ func getAWSClientRestConfig(cloudClients cloud.Clients, clock clockwork.Clock, r
}
}

// genAWSToken creates an AWS token to access EKS clusters.
// Logic from https://github.com/aws/aws-cli/blob/6c0d168f0b44136fc6175c57c090d4b115437ad1/awscli/customizations/eks/get_token.py#L211-L229
func genAWSToken(stsClient stsiface.STSAPI, clusterID string, clock clockwork.Clock) (string, time.Time, error) {
const (
// The sts GetCallerIdentity request is valid for 15 minutes regardless of this parameters value after it has been
// signed.
requestPresignParam = 60
// The actual token expiration (presigned STS urls are valid for 15 minutes after timestamp in x-amz-date).
presignedURLExpiration = 15 * time.Minute
v1Prefix = "k8s-aws-v1."
clusterIDHeader = "x-k8s-aws-id"
)

// generate an sts:GetCallerIdentity request and add our custom cluster ID header
request, _ := stsClient.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{})
request.HTTPRequest.Header.Add(clusterIDHeader, clusterID)

// Sign the request. The expires parameter (sets the x-amz-expires header) is
// currently ignored by STS, and the token expires 15 minutes after the x-amz-date
// timestamp regardless. We set it to 60 seconds for backwards compatibility (the
// parameter is a required argument to Presign(), and authenticators 0.3.0 and older are expecting a value between
// 0 and 60 on the server side).
// https://github.com/aws/aws-sdk-go/issues/2167
presignedURLString, err := request.Presign(requestPresignParam)
if err != nil {
return "", time.Time{}, trace.Wrap(err)
}

// Set token expiration to 1 minute before the presigned URL expires for some cushion
tokenExpiration := clock.Now().Add(presignedURLExpiration - 1*time.Minute)
return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration, nil
}

// getStaticCredentialsFromKubeconfig loads a kubeconfig from the cluster and returns the access credentials for the cluster.
// If the config defines multiple contexts, it will pick one (the order is not guaranteed).
func getStaticCredentialsFromKubeconfig(ctx context.Context, component KubeServiceType, cluster types.KubeCluster, log *logrus.Entry, checker servicecfg.ImpersonationPermissionsChecker) (*staticKubeCreds, error) {
Expand Down
62 changes: 62 additions & 0 deletions lib/kube/utils/eks_token_signed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Teleport
* Copyright (C) 2024 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 utils

import (
"encoding/base64"
"time"

"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
)

// GenAWSEKSToken creates an AWS token to access EKS clusters.
// Logic from https://github.com/aws/aws-cli/blob/6c0d168f0b44136fc6175c57c090d4b115437ad1/awscli/customizations/eks/get_token.py#L211-L229
func GenAWSEKSToken(stsClient stsiface.STSAPI, clusterID string, clock clockwork.Clock) (string, time.Time, error) {
const (
// The sts GetCallerIdentity request is valid for 15 minutes regardless of this parameters value after it has been
// signed.
requestPresignParam = 60
// The actual token expiration (presigned STS urls are valid for 15 minutes after timestamp in x-amz-date).
presignedURLExpiration = 15 * time.Minute
v1Prefix = "k8s-aws-v1."
clusterIDHeader = "x-k8s-aws-id"
)

// generate an sts:GetCallerIdentity request and add our custom cluster ID header
request, _ := stsClient.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{})
request.HTTPRequest.Header.Add(clusterIDHeader, clusterID)

// Sign the request. The expires parameter (sets the x-amz-expires header) is
// currently ignored by STS, and the token expires 15 minutes after the x-amz-date
// timestamp regardless. We set it to 60 seconds for backwards compatibility (the
// parameter is a required argument to Presign(), and authenticators 0.3.0 and older are expecting a value between
// 0 and 60 on the server side).
// https://github.com/aws/aws-sdk-go/issues/2167
presignedURLString, err := request.Presign(requestPresignParam)
if err != nil {
return "", time.Time{}, trace.Wrap(err)
}

// Set token expiration to 1 minute before the presigned URL expires for some cushion
tokenExpiration := clock.Now().Add(presignedURLExpiration - 1*time.Minute)
return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to call strings.TrimRight ? rstrip('=')

Suggested change
return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration, nil
return v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)), tokenExpiration, nil

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not required. = represents padding in b64 and it works anyway.
aws cli strips them but iam authenticator doesn't

}
2 changes: 1 addition & 1 deletion lib/srv/discovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1354,7 +1354,7 @@ func TestDiscoveryServer_New(t *testing.T) {
discServer, err := New(
ctx,
&Config{
CloudClients: nil,
CloudClients: tt.cloudClients,
ClusterFeatures: func() proto.Features { return proto.Features{} },
AccessPoint: newFakeAccessPoint(),
Matchers: tt.matchers,
Expand Down
Loading