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

Update usage of AWS SDK in aws_pca UpstreamAuthority plugin to v2 #2766

Merged
merged 16 commits into from
Jul 7, 2022
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ require (
github.com/Microsoft/go-winio v0.5.2
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129
github.com/armon/go-metrics v0.4.0
github.com/aws/aws-sdk-go v1.44.0
github.com/aws/aws-sdk-go-v2 v1.16.7
github.com/aws/aws-sdk-go-v2/config v1.15.13
github.com/aws/aws-sdk-go-v2/credentials v1.12.8
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8
github.com/aws/aws-sdk-go-v2/service/acmpca v1.17.10
github.com/aws/aws-sdk-go-v2/service/ec2 v1.47.2
github.com/aws/aws-sdk-go-v2/service/iam v1.18.9
github.com/aws/aws-sdk-go-v2/service/kms v1.17.5
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,6 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.43.16/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.0 h1:jwtHuNqfnJxL4DKHBUVUmQlfueQqBW7oXP6yebZR/R0=
github.com/aws/aws-sdk-go v1.44.0/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.16.7 h1:zfBwXus3u14OszRxGcqCDS4MfMCv10e8SMJ2r8Xm0Ns=
github.com/aws/aws-sdk-go-v2 v1.16.7/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
github.com/aws/aws-sdk-go-v2/config v1.15.13 h1:CJH9zn/Enst7lDiGpoguVt0lZr5HcpNVlRJWbJ6qreo=
Expand All @@ -217,6 +215,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 h1:2J+jdlBJWEmTyAwC82Y
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8/go.mod h1:ZIV8GYoC6WLBW5KGs+o4rsc65/ozd+eQ0L31XF5VDwk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 h1:QquxR7NH3ULBsKC+NoTpilzbKKS+5AELfNREInbhvas=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15/go.mod h1:Tkrthp/0sNBShQQsamR7j/zY4p19tVTAs+nnqhH6R3c=
github.com/aws/aws-sdk-go-v2/service/acmpca v1.17.10 h1:S0Vf3M6Y70WJ6Gb/ZkuGQ9C3ErODIkehSxXOu3bTUVQ=
github.com/aws/aws-sdk-go-v2/service/acmpca v1.17.10/go.mod h1:NU1zsuI+UaQZi+nw7n2pNp42mFX2xcxO6YgbGyEgP14=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.47.2 h1:81hrDgbXHL44WdY6M/fHGXLlv17qTpOFzutXRVDEk3Y=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.47.2/go.mod h1:VoBcwURHnJVCWuXHdqVuG03i2lUlHJ5DTTqDSyCdEcc=
github.com/aws/aws-sdk-go-v2/service/iam v1.18.9 h1:pVHvEz+KIsTwRKufwvGZr90X/YJ7swVshaBZNY4ESIY=
Expand Down
63 changes: 40 additions & 23 deletions pkg/server/plugin/upstreamauthority/awspca/pca.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
"time"

"github.com/andres-erbsen/clock"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/acmpca"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/acmpca"
acmpcatypes "github.com/aws/aws-sdk-go-v2/service/acmpca/types"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl"
upstreamauthorityv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/upstreamauthority/v1"
Expand All @@ -31,8 +32,13 @@ const (
// The default CA signing template to use.
// The SPIRE server intermediate CA can sign end-entity SVIDs only.
defaultCASigningTemplateArn = "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1"
// Max certificate issuance wait duration
maxCertIssuanceWaitDur = 3 * time.Minute
)

type newACMPCAClientFunc func(context.Context, *Configuration) (PCAClient, error)
type certificateIssuedWaitRetryFunc func(context.Context, *acmpca.GetCertificateInput, *acmpca.GetCertificateOutput, error) (bool, error)

func BuiltIn() catalog.BuiltIn {
return builtin(New())
}
Expand Down Expand Up @@ -67,8 +73,9 @@ type PCAPlugin struct {
config *configuration

hooks struct {
clock clock.Clock
newClient func(config *Configuration) (PCAClient, error)
clock clock.Clock
newClient newACMPCAClientFunc
waitRetryFn certificateIssuedWaitRetryFunc
}
}

Expand All @@ -81,13 +88,14 @@ type configuration struct {

// New returns an instantiated plugin
func New() *PCAPlugin {
return newPlugin(newPCAClient)
return newPlugin(newPCAClient, nil)
}

func newPlugin(newClient func(config *Configuration) (PCAClient, error)) *PCAPlugin {
func newPlugin(newClient newACMPCAClientFunc, waitRetryFn certificateIssuedWaitRetryFunc) *PCAPlugin {
p := &PCAPlugin{}
p.hooks.clock = clock.New()
p.hooks.newClient = newClient
p.hooks.waitRetryFn = waitRetryFn
return p
}

Expand All @@ -112,22 +120,22 @@ func (p *PCAPlugin) Configure(ctx context.Context, req *configv1.ConfigureReques
}

// Create the client
pcaClient, err := p.hooks.newClient(config)
pcaClient, err := p.hooks.newClient(ctx, config)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create client: %v", err)
}

// Perform a check for the presence of the CA
p.log.Info("Looking up certificate authority from ACM", "certificate_authority_arn", config.CertificateAuthorityARN)
describeResponse, err := pcaClient.DescribeCertificateAuthorityWithContext(ctx, &acmpca.DescribeCertificateAuthorityInput{
describeResponse, err := pcaClient.DescribeCertificateAuthority(ctx, &acmpca.DescribeCertificateAuthorityInput{
CertificateAuthorityArn: aws.String(config.CertificateAuthorityARN),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to describe CertificateAuthority: %v", err)
}

// Ensure the CA is set to ACTIVE
caStatus := aws.StringValue(describeResponse.CertificateAuthority.Status)
caStatus := describeResponse.CertificateAuthority.Status
if caStatus != "ACTIVE" {
p.log.Warn("Certificate is in an invalid state for issuance",
"certificate_authority_arn", config.CertificateAuthorityARN,
Expand All @@ -138,7 +146,7 @@ func (p *PCAPlugin) Configure(ctx context.Context, req *configv1.ConfigureReques
// Otherwise, fall back to the pre-configured value on the CA
signingAlgorithm := config.SigningAlgorithm
if signingAlgorithm == "" {
signingAlgorithm = aws.StringValue(describeResponse.CertificateAuthority.CertificateAuthorityConfiguration.SigningAlgorithm)
signingAlgorithm = string(describeResponse.CertificateAuthority.CertificateAuthorityConfiguration.SigningAlgorithm)
p.log.Info("No signing algorithm specified, using the CA default", "signing_algorithm", signingAlgorithm)
}

Expand Down Expand Up @@ -186,13 +194,13 @@ func (p *PCAPlugin) MintX509CAAndSubscribe(request *upstreamauthorityv1.MintX509
p.log.Info("Submitting CSR to ACM", "signing_algorithm", config.signingAlgorithm)
validityPeriod := time.Second * time.Duration(request.PreferredTtl)

issueResponse, err := p.pcaClient.IssueCertificateWithContext(ctx, &acmpca.IssueCertificateInput{
issueResponse, err := p.pcaClient.IssueCertificate(ctx, &acmpca.IssueCertificateInput{
CertificateAuthorityArn: aws.String(config.certificateAuthorityArn),
SigningAlgorithm: aws.String(config.signingAlgorithm),
SigningAlgorithm: acmpcatypes.SigningAlgorithm(config.signingAlgorithm),
Csr: csrBuf.Bytes(),
TemplateArn: aws.String(config.caSigningTemplateArn),
Validity: &acmpca.Validity{
Type: aws.String(acmpca.ValidityPeriodTypeAbsolute),
Validity: &acmpcatypes.Validity{
Type: acmpcatypes.ValidityPeriodTypeAbsolute,
Value: aws.Int64(p.hooks.clock.Now().Add(validityPeriod).Unix()),
},
})
Expand All @@ -204,36 +212,45 @@ func (p *PCAPlugin) MintX509CAAndSubscribe(request *upstreamauthorityv1.MintX509
// the certificate has been issued
certificateArn := issueResponse.CertificateArn

p.log.Info("Waiting for issuance from ACM", "certificate_arn", aws.StringValue(certificateArn))
p.log.Info("Waiting for issuance from ACM", "certificate_arn", aws.ToString(certificateArn))
getCertificateInput := &acmpca.GetCertificateInput{
CertificateAuthorityArn: aws.String(config.certificateAuthorityArn),
CertificateArn: certificateArn,
}
err = p.pcaClient.WaitUntilCertificateIssuedWithContext(ctx, getCertificateInput)
if err != nil {

var certIssuedWaitOptFns []func(*acmpca.CertificateIssuedWaiterOptions)
if p.hooks.waitRetryFn != nil {
retryableOption := func(opts *acmpca.CertificateIssuedWaiterOptions) {
opts.Retryable = p.hooks.waitRetryFn
}
certIssuedWaitOptFns = append(certIssuedWaitOptFns, retryableOption)
}

waiter := acmpca.NewCertificateIssuedWaiter(p.pcaClient, certIssuedWaitOptFns...)
if err := waiter.Wait(ctx, getCertificateInput, maxCertIssuanceWaitDur); err != nil {
return status.Errorf(codes.Internal, "failed waiting for issuance: %v", err)
}
p.log.Info("Certificate issued", "certificate_arn", aws.StringValue(certificateArn))
p.log.Info("Certificate issued", "certificate_arn", aws.ToString(certificateArn))

// Finally get the certificate contents
p.log.Info("Retrieving certificate and chain from ACM", "certificate_arn", aws.StringValue(certificateArn))
getResponse, err := p.pcaClient.GetCertificateWithContext(ctx, getCertificateInput)
p.log.Info("Retrieving certificate and chain from ACM", "certificate_arn", aws.ToString(certificateArn))
getResponse, err := p.pcaClient.GetCertificate(ctx, getCertificateInput)
if err != nil {
return status.Errorf(codes.Internal, "failed to get cerficates: %v", err)
}

// Parse the cert from the response
cert, err := pemutil.ParseCertificate([]byte(aws.StringValue(getResponse.Certificate)))
cert, err := pemutil.ParseCertificate([]byte(aws.ToString(getResponse.Certificate)))
if err != nil {
return status.Errorf(codes.Internal, "failed to parse certificate from response: %v", err)
}

// Parse the chain from the response
certChain, err := pemutil.ParseCertificates([]byte(aws.StringValue(getResponse.CertificateChain)))
certChain, err := pemutil.ParseCertificates([]byte(aws.ToString(getResponse.CertificateChain)))
if err != nil {
return status.Errorf(codes.Internal, "failed to parse certificate chain from response: %v", err)
}
p.log.Info("Certificate and chain received", "certificate_arn", aws.StringValue(certificateArn))
p.log.Info("Certificate and chain received", "certificate_arn", aws.ToString(certificateArn))

// ACM's API outputs the certificate chain from a GetCertificate call in the following
// order: A (signed by B) -> B (signed by ROOT) -> ROOT.
Expand Down
79 changes: 50 additions & 29 deletions pkg/server/plugin/upstreamauthority/awspca/pca_client.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,70 @@
package awspca

import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/acmpca"
"github.com/aws/aws-sdk-go/service/sts"
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/acmpca"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

// PCAClient provides an interface which can be mocked to test
// the functionality of the plugin.
type PCAClient interface {
DescribeCertificateAuthorityWithContext(aws.Context, *acmpca.DescribeCertificateAuthorityInput, ...request.Option) (*acmpca.DescribeCertificateAuthorityOutput, error)
IssueCertificateWithContext(aws.Context, *acmpca.IssueCertificateInput, ...request.Option) (*acmpca.IssueCertificateOutput, error)
WaitUntilCertificateIssuedWithContext(aws.Context, *acmpca.GetCertificateInput, ...request.WaiterOption) error
GetCertificateWithContext(aws.Context, *acmpca.GetCertificateInput, ...request.Option) (*acmpca.GetCertificateOutput, error)
DescribeCertificateAuthority(context.Context, *acmpca.DescribeCertificateAuthorityInput, ...func(*acmpca.Options)) (*acmpca.DescribeCertificateAuthorityOutput, error)
IssueCertificate(context.Context, *acmpca.IssueCertificateInput, ...func(*acmpca.Options)) (*acmpca.IssueCertificateOutput, error)
GetCertificate(context.Context, *acmpca.GetCertificateInput, ...func(*acmpca.Options)) (*acmpca.GetCertificateOutput, error)
}

func newPCAClient(config *Configuration) (PCAClient, error) {
awsConfig := &aws.Config{
Region: aws.String(config.Region),
Endpoint: aws.String(config.Endpoint),
func newPCAClient(ctx context.Context, cfg *Configuration) (PCAClient, error) {
var opts []func(*config.LoadOptions) error
if cfg.Region != "" {
opts = append(opts, config.WithRegion(cfg.Region))
}

// Optional: Assuming role
if config.AssumeRoleARN != "" {
staticsess, err := session.NewSession(&aws.Config{Credentials: awsConfig.Credentials})
if err != nil {
return nil, err
}
awsConfig.Credentials = credentials.NewCredentials(&stscreds.AssumeRoleProvider{
Client: sts.New(staticsess),
RoleARN: config.AssumeRoleARN,
Duration: 15 * time.Minute,
if cfg.Endpoint != "" {
endpointResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
if service == acmpca.ServiceID && region == cfg.Region {
return aws.Endpoint{
PartitionID: "aws",
URL: cfg.Endpoint,
SigningRegion: region,
}, nil
}

return aws.Endpoint{}, fmt.Errorf("unknown endpoint %s requested for region %s", service, region)
})
opts = append(opts, config.WithEndpointResolverWithOptions(endpointResolver))
}

awsSession, err := session.NewSession(awsConfig)
awsCfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
return nil, err
}

return acmpca.New(awsSession), nil
if cfg.AssumeRoleARN != "" {
awsCfg, err = newAWSAssumeRoleConfig(ctx, cfg.Region, awsCfg, cfg.AssumeRoleARN)
if err != nil {
return nil, err
}
}

return acmpca.NewFromConfig(awsCfg), nil
}

func newAWSAssumeRoleConfig(ctx context.Context, region string, awsConf aws.Config, assumeRoleArn string) (aws.Config, error) {
var opts []func(*config.LoadOptions) error
if region != "" {
opts = append(opts, config.WithRegion(region))
}

stsClient := sts.NewFromConfig(awsConf)
opts = append(opts, config.WithCredentialsProvider(aws.NewCredentialsCache(
stscreds.NewAssumeRoleProvider(stsClient, assumeRoleArn))),
)

return config.LoadDefaultConfig(ctx, opts...)
}
19 changes: 5 additions & 14 deletions pkg/server/plugin/upstreamauthority/awspca/pca_client_fake.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package awspca

import (
"context"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/acmpca"
"github.com/aws/aws-sdk-go-v2/service/acmpca"
"github.com/stretchr/testify/require"
)

Expand All @@ -23,33 +22,25 @@ type pcaClientFake struct {
expectedGetCertificateInput *acmpca.GetCertificateInput
getCertificateOutput *acmpca.GetCertificateOutput
getCertificateErr error

waitUntilCertificateIssuedErr error
}

func (f *pcaClientFake) DescribeCertificateAuthorityWithContext(ctx aws.Context, input *acmpca.DescribeCertificateAuthorityInput, option ...request.Option) (*acmpca.DescribeCertificateAuthorityOutput, error) {
func (f *pcaClientFake) DescribeCertificateAuthority(ctx context.Context, input *acmpca.DescribeCertificateAuthorityInput, optFns ...func(*acmpca.Options)) (*acmpca.DescribeCertificateAuthorityOutput, error) {
require.Equal(f.t, f.expectedDescribeInput, input)
if f.describeCertificateErr != nil {
return nil, f.describeCertificateErr
}
return f.describeCertificateOutput, nil
}

func (f *pcaClientFake) IssueCertificateWithContext(ctx aws.Context, input *acmpca.IssueCertificateInput, option ...request.Option) (*acmpca.IssueCertificateOutput, error) {
func (f *pcaClientFake) IssueCertificate(ctx context.Context, input *acmpca.IssueCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.IssueCertificateOutput, error) {
require.Equal(f.t, f.expectedIssueInput, input)
if f.issueCertifcateErr != nil {
return nil, f.issueCertifcateErr
}
return f.issueCertificateOutput, nil
}

func (f *pcaClientFake) WaitUntilCertificateIssuedWithContext(ctx aws.Context, input *acmpca.GetCertificateInput, option ...request.WaiterOption) error {
require.Equal(f.t, f.expectedGetCertificateInput, input)

return f.waitUntilCertificateIssuedErr
}

func (f *pcaClientFake) GetCertificateWithContext(ctx aws.Context, input *acmpca.GetCertificateInput, option ...request.Option) (*acmpca.GetCertificateOutput, error) {
func (f *pcaClientFake) GetCertificate(ctx context.Context, input *acmpca.GetCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.GetCertificateOutput, error) {
require.Equal(f.t, f.expectedGetCertificateInput, input)
if f.getCertificateErr != nil {
return nil, f.getCertificateErr
Expand Down
Loading