Skip to content

Commit

Permalink
Merge pull request #16577 from justinsb/v1_v2_request_aws
Browse files Browse the repository at this point in the history
aws node handshake: support both v1 and v2 signatures, default to v1
  • Loading branch information
k8s-ci-robot authored May 31, 2024
2 parents fc91085 + 40f6cc8 commit de3734f
Show file tree
Hide file tree
Showing 4 changed files with 453 additions and 57 deletions.
112 changes: 108 additions & 4 deletions upup/pkg/fi/cloudup/awsup/aws_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,38 @@ limitations under the License.
package awsup

import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/sts"
smithyhttp "github.com/aws/smithy-go/transport/http"
"k8s.io/kops/pkg/bootstrap"
)

const AWSAuthenticationTokenPrefix = "x-aws-sts "
const AWSAuthenticationTokenPrefixV1 = "x-aws-sts "
const AWSAuthenticationTokenPrefixV2 = "x-aws-sts-v2 "

type awsAuthenticator struct {
// sts holds the AWS STS client, for signing V2 requests
sts *sts.Client

// region holds the AWS region in which we are running
region string

// credentialsProvider returns our AWS credentials, for sigining V1 requests
credentialsProvider aws.CredentialsProvider
}

var _ bootstrap.Authenticator = &awsAuthenticator{}
Expand All @@ -60,23 +74,86 @@ func NewAWSAuthenticator(ctx context.Context, region string) (bootstrap.Authenti
return nil, fmt.Errorf("failed to load aws config: %w", err)
}
return &awsAuthenticator{
sts: sts.NewFromConfig(config),
credentialsProvider: config.Credentials,
region: region,
sts: sts.NewFromConfig(config),
}, nil
}

// awsV1Token is the format of the V1 request, it matches http.Header
type awsV1Token map[string][]string

// awsV2Token is the format of the V2 request, it maps to the http request generated by STS GetCallerIdentity
type awsV2Token struct {
URL string `json:"url"`
Method string `json:"method"`
SignedHeader http.Header `json:"headers"`
}

func (a *awsAuthenticator) CreateToken(body []byte) (string, error) {
ctx := context.TODO()

// We sign with V1, for backwards compatibility.
// The issue is that if we upgrade the nodes before the control plane,
// the nodes are using v2 authentication against a v1 verifier.
// By having the server support v1 and v2, but the nodes continue to use
// v1 for now, we can introduce v2 support and then enable it in a few versions.
// The "nodes before control plane" is not the common case,
// and nodes at much higher versions is not guaranteed to be supported by kube,
// so once we are at kOps 1.32 this shoud be safe to flip to use V2.
// It's possibly safe at kOps 1.31 but that might need more careful analysis.
signWithV1 := true
if signWithV1 {
return a.createTokenV1(ctx, body)
}
return a.createTokenV2(ctx, body)
}

func (a *awsAuthenticator) createTokenV1(ctx context.Context, body []byte) (string, error) {
credentials, err := a.credentialsProvider.Retrieve(ctx)
if err != nil {
return "", fmt.Errorf("getting AWS credentials: %w", err)
}

host, err := a.getSTSHost(ctx)
if err != nil {
return "", fmt.Errorf("getting AWS STS url: %w", err)
}
stsURL := "https://" + host + "/"
region := a.region

req, err := signV1Request(ctx, stsURL, region, credentials, time.Now(), body)
if err != nil {
return "", fmt.Errorf("building (v1) signed request: %w", err)
}
headers, err := json.Marshal(req.Header)
if err != nil {
return "", fmt.Errorf("converting headers to json: %w", err)
}
return AWSAuthenticationTokenPrefixV1 + base64.StdEncoding.EncodeToString(headers), nil
}

func (a *awsAuthenticator) getSTSHost(ctx context.Context) (string, error) {
// An inefficient but reliable way to get the STS url
presignClient := sts.NewPresignClient(a.sts)
stsRequest, err := presignClient.PresignGetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
if err != nil {
return "", fmt.Errorf("building AWS STS presigned request: %w", err)
}
u, err := url.Parse(stsRequest.URL)
if err != nil {
return "", fmt.Errorf("parsing AWS STS url: %w", err)
}
return u.Host, err
}

func (a *awsAuthenticator) createTokenV2(ctx context.Context, body []byte) (string, error) {
sha := sha256.Sum256(body)

presignClient := sts.NewPresignClient(a.sts)

// Ensure the signature is only valid for this particular body content.
stsRequest, err := presignClient.PresignGetCallerIdentity(context.TODO(), &sts.GetCallerIdentityInput{}, func(po *sts.PresignOptions) {
stsRequest, err := presignClient.PresignGetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}, func(po *sts.PresignOptions) {
po.ClientOptions = append(po.ClientOptions, func(o *sts.Options) {
o.APIOptions = append(o.APIOptions, smithyhttp.AddHeaderValue("X-Kops-Request-SHA", base64.RawStdEncoding.EncodeToString(sha[:])))
})
Expand All @@ -95,5 +172,32 @@ func (a *awsAuthenticator) CreateToken(body []byte) (string, error) {
return "", fmt.Errorf("converting token to json: %w", err)
}

return AWSAuthenticationTokenPrefix + base64.StdEncoding.EncodeToString(token), nil
return AWSAuthenticationTokenPrefixV2 + base64.StdEncoding.EncodeToString(token), nil
}

func signV1Request(ctx context.Context, stsURL string, region string, credentials aws.Credentials, signingTime time.Time, kopsRequestBody []byte) (*http.Request, error) {
kopsRequestHash := sha256.Sum256(kopsRequestBody)
kopsRequestHashBase64 := base64.RawStdEncoding.EncodeToString(kopsRequestHash[:])

// V1 requests use a well-known body (and host)
body := []byte("Action=GetCallerIdentity&Version=2011-06-15")

bodyHash := sha256.Sum256(body)

signedRequest, err := http.NewRequest("POST", stsURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("building http request: %v", err)
}
signedRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
signedRequest.Header.Add("X-Kops-Request-Sha", kopsRequestHashBase64)

signer := v4.NewSigner()

service := "sts"

if err := signer.SignHTTP(ctx, credentials, signedRequest, hex.EncodeToString(bodyHash[:]), service, region, signingTime); err != nil {
return nil, fmt.Errorf("error from SignHTTP: %v", err)
}

return signedRequest, nil
}
Loading

0 comments on commit de3734f

Please sign in to comment.