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

[GEP-26] Add support for workload identity #47

Merged
merged 3 commits into from
Nov 22, 2024
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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ Usage of ./aws-custom-route-controller:
--tick-period duration tick period for checking for updates (default 5s)
```

The AWS credentials are loaded from a secret using the control plane kubeconfig. The secret needs to provide the data keys `accessKeyID` and `secretAccessKey`.
The AWS access key must have permissions to describe route tables of the cluster and to create and delete routes.
The AWS credentials are loaded from a secret using the control plane kubeconfig.
The secret needs to provide one of the following combinations:
- the data keys `accessKeyID` and `secretAccessKey`
- the data keys `roleARN` and `workloadIdentityTokenFile`

The AWS credentials must have permissions to describe route tables of the cluster and to create and delete routes.

## What is it good for?

Expand Down
74 changes: 69 additions & 5 deletions pkg/updater/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ package updater
import (
"context"
"fmt"
"os"

"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
Expand All @@ -22,13 +24,39 @@ const (
AccessKeyID = "accessKeyID"
// SecretAccessKey is a constant for the key in a cloud provider secret and backup secret that holds the AWS secret access key.
SecretAccessKey = "secretAccessKey"
// WorkloadIdentityTokenFile is a constant for the key in a cloud provider secret and backup secret that holds the path to a workload identity token.
WorkloadIdentityTokenFile = "workloadIdentityTokenFile"
// RoleARN is a constant for the key in a cloud provider secret and backup secret that holds ARN of a role that is to be assumed.
RoleARN = "roleARN"
// InClusterConfig is a special name for the kubeconfig to use in-cluster client
InClusterConfig = "inClusterConfig"
)

type Credentials struct {
AccessKeyID string
SecretAccessKey string
// AccessKey represents static credentials for authentication to AWS.
// This field is mutually exclusive with WorkloadIdentity.
AccessKey *AccessKey

// WorkloadIdentity contains workload identity configuration.
// This field is mutually exclusive with AccessKey.
WorkloadIdentity *WorkloadIdentity
}

// AccessKey represents static credentials for authentication to AWS.
type AccessKey struct {
// ID is the key ID used for access to AWS.
ID string
// Secret is the secret used for access to AWS.
Secret string
}

// WorkloadIdentity contains workload identity configuration for authentication to AWS.
type WorkloadIdentity struct {
// TokenRetriever a function that retrieves a token used for exchanging AWS credentials.
TokenRetriever stscreds.IdentityTokenRetriever

// RoleARN is the ARN of the role that will be assumed.
RoleARN string
}

func LoadCredentials(controlKubeconfig, namespace, secretName string) (*Credentials, error) {
Expand All @@ -53,14 +81,38 @@ func LoadCredentials(controlKubeconfig, namespace, secretName string) (*Credenti
return nil, err
}

return extractCredentials(secret)
creds, err := extractCredentials(secret)
if err != nil {
return nil, err
}
return creds, nil
}

func extractCredentials(secret *corev1.Secret) (*Credentials, error) {
if secret.Data == nil {
return nil, fmt.Errorf("secret does not contain any data")
}

if workloadIdentityTokenFile, ok := secret.Data[WorkloadIdentityTokenFile]; ok {
if len(workloadIdentityTokenFile) == 0 {
return nil, fmt.Errorf("workloadIdentityTokenFile must not be empty")
}

roleARN, ok := secret.Data[RoleARN]
if !ok || len(roleARN) == 0 {
return nil, fmt.Errorf("roleARN is required")
}

return &Credentials{
WorkloadIdentity: &WorkloadIdentity{
TokenRetriever: &fileTokenRetriever{
fileName: string(workloadIdentityTokenFile),
},
RoleARN: string(roleARN),
},
}, nil
}

accessKeyID, err := getSecretDataValue(secret, AccessKeyID, nil, true)
if err != nil {
return nil, err
Expand All @@ -72,8 +124,10 @@ func extractCredentials(secret *corev1.Secret) (*Credentials, error) {
}

return &Credentials{
AccessKeyID: string(accessKeyID),
SecretAccessKey: string(secretAccessKey),
AccessKey: &AccessKey{
ID: string(accessKeyID),
Secret: string(secretAccessKey),
},
}, nil
}

Expand All @@ -94,3 +148,13 @@ func getSecretDataValue(secret *corev1.Secret, key string, altKey *string, requi
}
return nil, nil
}

type fileTokenRetriever struct {
fileName string
}

var _ stscreds.IdentityTokenRetriever = (*fileTokenRetriever)(nil)

func (f *fileTokenRetriever) GetIdentityToken() ([]byte, error) {
return os.ReadFile(f.fileName)
}
19 changes: 18 additions & 1 deletion pkg/updater/ec2.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ package updater

import (
"context"
"errors"

"github.com/aws/aws-sdk-go-v2/aws"
v2config "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

// TagNameKubernetesClusterPrefix is the tag name we use to differentiate multiple
Expand All @@ -37,10 +40,24 @@ type EC2Routes interface {
}

func NewAWSEC2Routes(creds *Credentials, region string) (EC2Routes, error) {
var credentialsProvider aws.CredentialsProvider
switch {
case creds.AccessKey != nil:
credentialsProvider = credentials.NewStaticCredentialsProvider(creds.AccessKey.ID, creds.AccessKey.Secret, "")
case creds.WorkloadIdentity != nil:
credentialsProvider = stscreds.NewWebIdentityRoleProvider(
sts.NewFromConfig(aws.Config{Region: region}),
creds.WorkloadIdentity.RoleARN,
creds.WorkloadIdentity.TokenRetriever,
)
default:
return nil, errors.New("credentials should either contain access key or workload identity config")
}

cfg, err := v2config.LoadDefaultConfig(
context.TODO(),
v2config.WithRegion(region),
v2config.WithCredentialsProvider(aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, ""))),
v2config.WithCredentialsProvider(aws.NewCredentialsCache(credentialsProvider)),
)
if err != nil {
return nil, err
Expand Down