Skip to content

Commit 06f8258

Browse files
Merge pull request #405 from haarchri/feature/AWSWebIdentityCredentials
feat(aws): implement AWSWebIdentityCredentials
2 parents 9bcfc25 + 06f9ca2 commit 06f8258

File tree

11 files changed

+391
-4
lines changed

11 files changed

+391
-4
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
apiVersion: kubernetes.crossplane.io/v1alpha1
2+
kind: ProviderConfig
3+
metadata:
4+
name: kubernetes-provider
5+
spec:
6+
credentials:
7+
source: Secret
8+
secretRef:
9+
namespace: crossplane-system
10+
name: cluster-config
11+
key: kubeconfig
12+
identity:
13+
source: InjectedIdentity
14+
type: AWSWebIdentityCredentials
15+
---
16+
apiVersion: v1
17+
kind: Secret
18+
metadata:
19+
name: cluster-config
20+
namespace: crossplane-system
21+
type: Opaque
22+
stringData:
23+
# The cluster name must be the actual EKS cluster name or the full ARN.
24+
# Examples:
25+
# - name: my-cluster-name
26+
# - name: arn:aws:eks:us-west-2:123456789012:cluster/my-cluster-name
27+
kubeconfig: |
28+
apiVersion: v1
29+
kind: Config
30+
clusters:
31+
- cluster:
32+
certificate-authority-data: LS0tLS1CRUdJTi...BASE64_ENCODED_CA...
33+
server: https://ABCD1234EFGH5678.gr7.us-west-2.eks.amazonaws.com
34+
name: eks-cluster
35+
contexts:
36+
- context:
37+
cluster: eks-cluster
38+
user: eks-user
39+
name: eks-context
40+
current-context: eks-context
41+
users:
42+
- name: eks-user
43+
user: {}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
apiVersion: kubernetes.m.crossplane.io/v1alpha1
2+
kind: ProviderConfig
3+
metadata:
4+
name: kubernetes-provider
5+
namespace: crossplane-system
6+
spec:
7+
credentials:
8+
source: Secret
9+
secretRef:
10+
namespace: crossplane-system
11+
name: cluster-config
12+
key: kubeconfig
13+
identity:
14+
source: InjectedIdentity
15+
type: AWSWebIdentityCredentials
16+
---
17+
apiVersion: v1
18+
kind: Secret
19+
metadata:
20+
name: cluster-config
21+
namespace: crossplane-system
22+
type: Opaque
23+
stringData:
24+
# The cluster name must be the actual EKS cluster name or the full ARN.
25+
# Examples:
26+
# - name: my-cluster-name
27+
# - name: arn:aws:eks:us-west-2:123456789012:cluster/my-cluster-name
28+
kubeconfig: |
29+
apiVersion: v1
30+
kind: Config
31+
clusters:
32+
- cluster:
33+
certificate-authority-data: LS0tLS1CRUdJTi...BASE64_ENCODED_CA...
34+
server: https://ABCD1234EFGH5678.gr7.us-west-2.eks.amazonaws.com
35+
name: eks-cluster
36+
contexts:
37+
- context:
38+
cluster: eks-cluster
39+
user: eks-user
40+
name: eks-context
41+
current-context: eks-context
42+
users:
43+
- name: eks-user
44+
user: {}

go.mod

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ go 1.24.6
55
require (
66
github.com/Azure/kubelogin v0.1.4
77
github.com/alecthomas/kingpin/v2 v2.4.0
8+
github.com/aws/aws-sdk-go-v2 v1.39.4
9+
github.com/aws/aws-sdk-go-v2/config v1.31.15
10+
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9
11+
github.com/aws/smithy-go v1.23.1
812
github.com/crossplane/crossplane-runtime/v2 v2.0.0-rc.1
913
github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec
1014
github.com/google/cel-go v0.23.2
@@ -44,6 +48,15 @@ require (
4448
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
4549
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
4650
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
51+
github.com/aws/aws-sdk-go-v2/credentials v1.18.19 // indirect
52+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 // indirect
53+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 // indirect
54+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 // indirect
55+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
56+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect
57+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 // indirect
58+
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 // indirect
59+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 // indirect
4760
github.com/beorn7/perks v1.0.1 // indirect
4861
github.com/blang/semver/v4 v4.0.0 // indirect
4962
github.com/cespare/xxhash/v2 v2.3.0 // indirect

go.sum

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vS
3636
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
3737
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
3838
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
39+
github.com/aws/aws-sdk-go-v2 v1.39.4 h1:qTsQKcdQPHnfGYBBs+Btl8QwxJeoWcOcPcixK90mRhg=
40+
github.com/aws/aws-sdk-go-v2 v1.39.4/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
41+
github.com/aws/aws-sdk-go-v2/config v1.31.15 h1:gE3M4xuNXfC/9bG4hyowGm/35uQTi7bUKeYs5e/6uvU=
42+
github.com/aws/aws-sdk-go-v2/config v1.31.15/go.mod h1:HvnvGJoE2I95KAIW8kkWVPJ4XhdrlvwJpV6pEzFQa8o=
43+
github.com/aws/aws-sdk-go-v2/credentials v1.18.19 h1:Jc1zzwkSY1QbkEcLujwqRTXOdvW8ppND3jRBb/VhBQc=
44+
github.com/aws/aws-sdk-go-v2/credentials v1.18.19/go.mod h1:DIfQ9fAk5H0pGtnqfqkbSIzky82qYnGvh06ASQXXg6A=
45+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 h1:X7X4YKb+c0rkI6d4uJ5tEMxXgCZ+jZ/D6mvkno8c8Uw=
46+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11/go.mod h1:EqM6vPZQsZHYvC4Cai35UDg/f5NCEU+vp0WfbVqVcZc=
47+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 h1:7AANQZkF3ihM8fbdftpjhken0TP9sBzFbV/Ze/Y4HXA=
48+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11/go.mod h1:NTF4QCGkm6fzVwncpkFQqoquQyOolcyXfbpC98urj+c=
49+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 h1:ShdtWUZT37LCAA4Mw2kJAJtzaszfSHFb5n25sdcv4YE=
50+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11/go.mod h1:7bUb2sSr2MZ3M/N+VyETLTQtInemHXb/Fl3s8CLzm0Y=
51+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
52+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
53+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4=
54+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ=
55+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 h1:GpMf3z2KJa4RnJ0ew3Hac+hRFYLZ9DDjfgXjuW+pB54=
56+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11/go.mod h1:6MZP3ZI4QQsgUCFTwMZA2V0sEriNQ8k2hmoHF3qjimQ=
57+
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 h1:M5nimZmugcZUO9wG7iVtROxPhiqyZX6ejS1lxlDPbTU=
58+
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8/go.mod h1:mbef/pgKhtKRwrigPPs7SSSKZgytzP8PQ6P6JAAdqyM=
59+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 h1:S5GuJZpYxE0lKeMHKn+BRTz6PTFpgThyJ+5mYfux7BM=
60+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3/go.mod h1:X4OF+BTd7HIb3L+tc4UlWHVrpgwZZIVENU15pRDVTI0=
61+
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 h1:Ekml5vGg6sHSZLZJQJagefnVe6PmqC2oiRkBq4F7fU0=
62+
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9/go.mod h1:/e15V+o1zFHWdH3u7lpI3rVBcxszktIKuHKCY2/py+k=
63+
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
64+
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
3965
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
4066
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
4167
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=

package/crds/kubernetes.crossplane.io_providerconfigs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ spec:
172172
- AzureServicePrincipalCredentials
173173
- AzureWorkloadIdentityCredentials
174174
- UpboundTokens
175+
- AWSWebIdentityCredentials
175176
type: string
176177
required:
177178
- source

package/crds/kubernetes.m.crossplane.io_clusterproviderconfigs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ spec:
172172
- AzureServicePrincipalCredentials
173173
- AzureWorkloadIdentityCredentials
174174
- UpboundTokens
175+
- AWSWebIdentityCredentials
175176
type: string
176177
required:
177178
- source

package/crds/kubernetes.m.crossplane.io_providerconfigs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ spec:
172172
- AzureServicePrincipalCredentials
173173
- AzureWorkloadIdentityCredentials
174174
- UpboundTokens
175+
- AWSWebIdentityCredentials
175176
type: string
176177
required:
177178
- source

pkg/kube/client/aws/aws.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
Copyright 2025 The Crossplane Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
// Package aws contains utilities for authenticating to EKS clusters.
15+
package aws
16+
17+
import (
18+
"context"
19+
"encoding/base64"
20+
"fmt"
21+
"net/http"
22+
"strings"
23+
24+
"github.com/aws/aws-sdk-go-v2/aws/arn"
25+
"github.com/aws/aws-sdk-go-v2/config"
26+
"github.com/aws/aws-sdk-go-v2/service/sts"
27+
smithyhttp "github.com/aws/smithy-go/transport/http"
28+
"github.com/pkg/errors"
29+
"k8s.io/client-go/rest"
30+
)
31+
32+
const (
33+
// clusterIDHeader is the header name for the cluster ID
34+
clusterIDHeader = "x-k8s-aws-id"
35+
// expireHeader is the header name for the expiration time
36+
expireHeader = "X-Amz-Expires"
37+
// tokenPrefix is the prefix for the EKS token
38+
tokenPrefix = "k8s-aws-v1."
39+
// tokenExpiration is the default expiration time for EKS tokens (15 minutes)
40+
tokenExpiration = 900
41+
)
42+
43+
// WrapRESTConfig configures the supplied REST config to use bearer tokens
44+
// fetched using AWS credentials chain for EKS authentication.
45+
// This uses AWS Web Identity / IRSA to assume a role that has access to the EKS cluster.
46+
// clusterNameFromKubeconfig is the cluster name from the kubeconfig (can be an ARN or plain name).
47+
func WrapRESTConfig(ctx context.Context, rc *rest.Config, clusterNameFromKubeconfig string) error {
48+
// Extract cluster name from ARN if needed
49+
clusterName, err := extractClusterNameFromARN(clusterNameFromKubeconfig)
50+
if err != nil {
51+
return errors.Wrap(err, "failed to extract cluster name from kubeconfig")
52+
}
53+
54+
cfg, err := config.LoadDefaultConfig(ctx)
55+
if err != nil {
56+
return errors.Wrap(err, "failed to load AWS config using default credentials chain")
57+
}
58+
59+
// Create STS client with the credentials (which may already be assumed role credentials)
60+
stsClient := sts.NewFromConfig(cfg)
61+
62+
// Create a token source that generates EKS tokens on demand
63+
tokenSource := &eksTokenSource{
64+
ctx: ctx,
65+
stsClient: stsClient,
66+
clusterID: clusterName,
67+
}
68+
69+
// Wrap the transport to inject the bearer token
70+
rc.Wrap(func(rt http.RoundTripper) http.RoundTripper {
71+
return &bearerAuthRoundTripper{
72+
source: tokenSource,
73+
rt: rt,
74+
}
75+
})
76+
77+
// Clear any exec provider since we're handling auth ourselves
78+
rc.ExecProvider = nil
79+
80+
return nil
81+
}
82+
83+
// extractClusterNameFromARN extracts the cluster name from an EKS cluster ARN
84+
// ARN format: arn:aws:eks:region:account:cluster/cluster-name
85+
func extractClusterNameFromARN(arnString string) (string, error) {
86+
// Check if it's an ARN using AWS SDK
87+
if !arn.IsARN(arnString) {
88+
// Not an ARN, might be just the cluster name
89+
return arnString, nil
90+
}
91+
92+
// Parse ARN using AWS SDK
93+
parsedARN, err := arn.Parse(arnString)
94+
if err != nil {
95+
return "", errors.Wrap(err, "failed to parse ARN")
96+
}
97+
98+
// EKS cluster ARNs have Resource in format "cluster/cluster-name"
99+
// Split by '/' to get the cluster name
100+
parts := strings.Split(parsedARN.Resource, "/")
101+
if len(parts) < 2 {
102+
return "", fmt.Errorf("invalid EKS cluster ARN resource format: %s", parsedARN.Resource)
103+
}
104+
105+
return parts[len(parts)-1], nil
106+
}
107+
108+
// eksTokenSource generates EKS authentication tokens using AWS STS
109+
type eksTokenSource struct {
110+
ctx context.Context
111+
stsClient *sts.Client
112+
clusterID string
113+
}
114+
115+
// Token generates an EKS authentication token
116+
// This replicates the behavior of `aws eks get-token` command
117+
// The STS client uses credentials from the AWS default credentials chain,
118+
// which includes assumed role credentials from Web Identity/IRSA
119+
func (s *eksTokenSource) Token() (string, error) {
120+
// Create a presigned request for GetCallerIdentity
121+
// This is what EKS uses for authentication
122+
// Default expiration is 15 minutes (900 seconds) which is what EKS expects
123+
presigner := sts.NewPresignClient(s.stsClient)
124+
125+
// Create presigned request with cluster ID and expiration headers
126+
// This matches the provider-aws implementation exactly
127+
presignedReq, err := presigner.PresignGetCallerIdentity(s.ctx,
128+
&sts.GetCallerIdentityInput{},
129+
func(po *sts.PresignOptions) {
130+
po.ClientOptions = []func(*sts.Options){
131+
sts.WithAPIOptions(
132+
smithyhttp.AddHeaderValue(clusterIDHeader, s.clusterID),
133+
smithyhttp.AddHeaderValue(expireHeader, fmt.Sprintf("%d", tokenExpiration)),
134+
),
135+
}
136+
})
137+
if err != nil {
138+
return "", errors.Wrap(err, "failed to presign GetCallerIdentity request")
139+
}
140+
141+
// Encode the presigned URL as a base64 token with the EKS prefix
142+
token := tokenPrefix + base64.RawURLEncoding.EncodeToString([]byte(presignedReq.URL))
143+
144+
return token, nil
145+
}
146+
147+
// bearerAuthRoundTripper injects a bearer token into HTTP requests
148+
type bearerAuthRoundTripper struct {
149+
source *eksTokenSource
150+
rt http.RoundTripper
151+
}
152+
153+
// RoundTrip implements http.RoundTripper
154+
func (b *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
155+
token, err := b.source.Token()
156+
if err != nil {
157+
return nil, errors.Wrap(err, "failed to get EKS token")
158+
}
159+
160+
// Clone the request and add the bearer token
161+
reqCopy := req.Clone(req.Context())
162+
reqCopy.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
163+
164+
return b.rt.RoundTrip(reqCopy)
165+
}

0 commit comments

Comments
 (0)