Skip to content

Commit

Permalink
AWS IAM role: Generate temporary security credentials from Role (#350)
Browse files Browse the repository at this point in the history
* AWS IAM: Generate Temp creds from Role

* fix error

* remove unit test for role since it fails

* Add unit test for restic

* more tests and refactoring

* fix errors

* Adding the missing file & fix error

* IAM role support for blockstorage + refactoring

* remove static credential creation

* create test profile with role

* Update pkg/config/aws/role.go

Co-Authored-By: Thomas Manville <tom@kasten.io>

* Update pkg/config/aws/aws.go

Co-Authored-By: Thomas Manville <tom@kasten.io>

* Address review suggestions

* Trivial:Update comment

* Switch role in objectstore if env variable role set

* refresh aws blockstorage creds + avoid objectsore errors

* nit: import time

* Some more unit test

* fix ci errors

* Skip objectsotre_test for listing bucket

* remove refresh creds func

* uncomment tests

* nit
  • Loading branch information
SupriyaKasten authored and mergify[bot] committed Oct 24, 2019
1 parent 47338da commit 2733489
Show file tree
Hide file tree
Showing 22 changed files with 328 additions and 142 deletions.
23 changes: 9 additions & 14 deletions pkg/blockstorage/awsebs/awsebs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
Expand Down Expand Up @@ -61,32 +60,28 @@ func (s *ebsStorage) Type() blockstorage.Type {
}

// NewProvider returns a provider for the EBS storage type in the specified region
func NewProvider(config map[string]string) (blockstorage.Provider, error) {
awsConfig, region, role, err := awsconfig.GetConfig(config)
func NewProvider(ctx context.Context, config map[string]string) (blockstorage.Provider, error) {
awsConfig, region, err := awsconfig.GetConfig(ctx, config)
if err != nil {
return nil, err
}
ec2Cli, err := newEC2Client(region, awsConfig, role)
ec2Cli, err := newEC2Client(region, awsConfig)
if err != nil {
return nil, errors.Wrapf(err, "Could not get EC2 client")
}
return &ebsStorage{ec2Cli: ec2Cli, role: role}, nil
return &ebsStorage{ec2Cli: ec2Cli, role: config[awsconfig.ConfigRole]}, nil
}

// newEC2Client returns ec2 client struct.
func newEC2Client(awsRegion string, config *aws.Config, role string) (*EC2, error) {
func newEC2Client(awsRegion string, config *aws.Config) (*EC2, error) {
if config == nil {
return nil, errors.New("Invalid empty AWS config")
}
s, err := session.NewSession(config)
if err != nil {
return nil, errors.Wrap(err, "Failed to create session for EFS")
}
creds := config.Credentials
if role != "" {
creds = stscreds.NewCredentials(s, role)
}
conf := config.WithMaxRetries(maxRetries).WithRegion(awsRegion).WithCredentials(creds)
conf := config.WithMaxRetries(maxRetries).WithRegion(awsRegion).WithCredentials(config.Credentials)
return &EC2{EC2: ec2.New(s, conf)}, nil
}

Expand Down Expand Up @@ -229,15 +224,15 @@ func (s *ebsStorage) SnapshotCopy(ctx context.Context, from, to blockstorage.Sna
return nil, errors.Errorf("Snapshot %v destination ID must be empty", to)
}
// Copy operation must be initiated from the destination region.
ec2Cli, err := newEC2Client(to.Region, s.ec2Cli.Config.Copy(), "")
ec2Cli, err := newEC2Client(to.Region, s.ec2Cli.Config.Copy())
if err != nil {
return nil, errors.Wrapf(err, "Could not get EC2 client")
}
// Include a presigned URL when the regions are different. Include it
// independent of whether or not the snapshot is encrypted.
var presignedURL *string
if to.Region != from.Region {
fromCli, err2 := newEC2Client(from.Region, s.ec2Cli.Config.Copy(), "")
fromCli, err2 := newEC2Client(from.Region, s.ec2Cli.Config.Copy())
if err2 != nil {
return nil, errors.Wrap(err2, "Could not create client to presign URL for snapshot copy request")
}
Expand Down Expand Up @@ -599,7 +594,7 @@ func (s *ebsStorage) FromRegion(ctx context.Context, region string) ([]string, e
}

func (s *ebsStorage) queryRegionToZones(ctx context.Context, region string) ([]string, error) {
ec2Cli, err := newEC2Client(region, s.ec2Cli.Config.Copy(), "")
ec2Cli, err := newEC2Client(region, s.ec2Cli.Config.Copy())
if err != nil {
return nil, errors.Wrapf(err, "Could not get EC2 client")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/blockstorage/awsebs/awsebs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (s AWSEBSSuite) TestQueryRegionToZones(c *C) {
c.Skip("Only works on AWS")
ctx := context.Background()
region := "us-east-1"
ec2Cli, err := newEC2Client(region, aws.NewConfig().WithCredentials(credentials.NewEnvCredentials()), "")
ec2Cli, err := newEC2Client(region, aws.NewConfig().WithCredentials(credentials.NewEnvCredentials()))
c.Assert(err, IsNil)
provider := &ebsStorage{ec2Cli: ec2Cli}
zs, err := provider.queryRegionToZones(ctx, region)
Expand Down
15 changes: 6 additions & 9 deletions pkg/blockstorage/awsefs/awsefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/backup"
awsefs "github.com/aws/aws-sdk-go/service/efs"
Expand All @@ -39,6 +38,7 @@ type efs struct {
*backup.Backup
accountID string
region string
role string
}

var _ blockstorage.Provider = (*efs)(nil)
Expand All @@ -60,8 +60,8 @@ const (
)

// NewEFSProvider retuns a blockstorage provider for AWS EFS.
func NewEFSProvider(config map[string]string) (blockstorage.Provider, error) {
awsConfig, region, role, err := awsconfig.GetConfig(config)
func NewEFSProvider(ctx context.Context, config map[string]string) (blockstorage.Provider, error) {
awsConfig, region, err := awsconfig.GetConfig(ctx, config)
if err != nil {
return nil, errors.Wrap(err, "Failed to get configuration for EFS")
}
Expand All @@ -78,17 +78,14 @@ func NewEFSProvider(config map[string]string) (blockstorage.Provider, error) {
return nil, errors.New("Account ID is empty")
}
accountID := *user.Account
creds := awsConfig.Credentials
if role != "" {
creds = stscreds.NewCredentials(s, role)
}
efsCli := awsefs.New(s, aws.NewConfig().WithRegion(region).WithCredentials(creds).WithMaxRetries(maxRetries))
backupCli := backup.New(s, aws.NewConfig().WithRegion(region).WithCredentials(creds).WithMaxRetries(maxRetries))
efsCli := awsefs.New(s, aws.NewConfig().WithRegion(region).WithCredentials(awsConfig.Credentials).WithMaxRetries(maxRetries))
backupCli := backup.New(s, aws.NewConfig().WithRegion(region).WithCredentials(awsConfig.Credentials).WithMaxRetries(maxRetries))
return &efs{
EFS: efsCli,
Backup: backupCli,
region: region,
accountID: accountID,
role: config[awsconfig.ConfigRole],
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/blockstorage/blockstorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ func (s *BlockStorageProviderSuite) getConfig(c *C, region string) map[string]st
}
config[awsconfig.AccessKeyID] = accessKey
config[awsconfig.SecretAccessKey] = secretAccessKey
config[awsconfig.ConfigRole] = os.Getenv(awsconfig.ConfigRole)
case blockstorage.TypeGPD:
creds, ok := os.LookupEnv(blockstorage.GoogleCloudCreds)
if !ok {
Expand Down
2 changes: 1 addition & 1 deletion pkg/blockstorage/getter/getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func New() Getter {
func (*getter) Get(storageType blockstorage.Type, config map[string]string) (blockstorage.Provider, error) {
switch storageType {
case blockstorage.TypeEBS:
return awsebs.NewProvider(config)
return awsebs.NewProvider(context.TODO(), config)
case blockstorage.TypeSoftlayerBlock:
return ibm.NewProvider(context.TODO(), config)
case blockstorage.TypeGPD:
Expand Down
36 changes: 27 additions & 9 deletions pkg/config/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
package aws

import (
"errors"
"context"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/pkg/errors"
)

const (
Expand All @@ -33,23 +35,39 @@ const (
SecretAccessKey = "AWS_SECRET_ACCESS_KEY"
// SessionToken represents AWS Session Key
SessionToken = "AWS_SESSION_TOKEN"

assumeRoleDuration = 25 * time.Minute
)

// GetConfig returns a configuration to establish AWS connection, connected region name and the role to assume if it exists.
func GetConfig(config map[string]string) (awsConfig *aws.Config, region string, role string, err error) {
// GetConfig returns a configuration to establish AWS connection and connected region name.
func GetConfig(ctx context.Context, config map[string]string) (awsConfig *aws.Config, region string, err error) {
region, ok := config[ConfigRegion]
if !ok {
return nil, "", "", errors.New("region required for storage type EBS")
return nil, "", errors.New("region required for storage type EBS/EFS")
}
accessKey, ok := config[AccessKeyID]
if !ok {
return nil, "", "", errors.New("AWS_ACCESS_KEY_ID required for storage type EBS")
return nil, "", errors.New("AWS_ACCESS_KEY_ID required for storage type EBS/EFS")
}
secretAccessKey, ok := config[SecretAccessKey]
if !ok {
return nil, "", "", errors.New("AWS_SECRET_ACCESS_KEY required for storage type EBS")
return nil, "", errors.New("AWS_SECRET_ACCESS_KEY required for storage type EBS/EFS")
}
role := config[ConfigRole]
if role != "" {
config, err := assumeRole(ctx, accessKey, secretAccessKey, role)
if err != nil {
return nil, "", errors.Wrap(err, "Failed to get temporary security credentials")
}
return config, region, nil
}
return &aws.Config{Credentials: credentials.NewStaticCredentials(accessKey, secretAccessKey, "")}, region, nil
}

func assumeRole(ctx context.Context, accessKey, secretAccessKey, role string) (*aws.Config, error) {
creds, err := SwitchRole(ctx, accessKey, secretAccessKey, role, assumeRoleDuration)
if err != nil {
return nil, errors.Wrap(err, "Failed to switch roles")
}
sessionToken := config[SessionToken]
role = config[ConfigRole]
return &aws.Config{Credentials: credentials.NewStaticCredentials(accessKey, secretAccessKey, sessionToken)}, region, role, nil
return &aws.Config{Credentials: creds}, nil
}
39 changes: 39 additions & 0 deletions pkg/config/aws/role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2019 The Kanister Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package aws

import (
"context"
"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/session"
"github.com/pkg/errors"
)

// SwitchRole func uses credentials API to automatically generates New Credentials for a given role.
func SwitchRole(ctx context.Context, accessKeyID string, secretAccessKey string, role string, duration time.Duration) (*credentials.Credentials, error) {
creds := credentials.NewStaticCredentials(accessKeyID, secretAccessKey, "")
sess, err := session.NewSession(aws.NewConfig().WithCredentials(creds))
if err != nil {
return nil, errors.Wrap(err, "Failed to create session")
}
creds = stscreds.NewCredentials(sess, role, func(p *stscreds.AssumeRoleProvider) {
p.Duration = duration
})
return creds, nil
}
17 changes: 17 additions & 0 deletions pkg/config/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package config

import (
"fmt"
"os"

"gopkg.in/check.v1"
)

func GetEnvOrSkip(c *check.C, varName string) string {
v := os.Getenv(varName)
if v == "" {
reason := fmt.Sprintf("Test %s requires the environemnt variable '%s'", c.TestName(), varName)
c.Skip(reason)
}
return v
}
27 changes: 26 additions & 1 deletion pkg/function/backup_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func newValidProfileWithSecretCredentials() *param.Profile {
Data: map[string][]byte{
secrets.AWSAccessKeyID: []byte("key-id"),
secrets.AWSSecretAccessKey: []byte("access-key"),
secrets.AWSSessionToken: []byte("session-token"),
secrets.ConfigRole: []byte("role"),
},
},
},
Expand All @@ -91,6 +91,30 @@ func newInvalidProfile() *param.Profile {
}
}

func newInvalidProfileWithSecretCredentials() *param.Profile {
return &param.Profile{
Location: crv1alpha1.Location{
Type: crv1alpha1.LocationTypeS3Compliant,
Bucket: "test-bucket",
Endpoint: "",
Prefix: "",
Region: "us-west-1",
},
Credential: param.Credential{
Type: param.CredentialTypeSecret,
Secret: &v1.Secret{
Type: v1.SecretType(secrets.AWSSecretType),
Data: map[string][]byte{
secrets.AWSAccessKeyID: []byte("key-id"),
secrets.AWSSecretAccessKey: []byte("access-key"),
secrets.ConfigRole: []byte("role"),
"InvalidSecretKey": []byte("InvalidValue"),
},
},
},
}
}

func (s *BackupDataSuite) TestValidateProfile(c *C) {
testCases := []struct {
name string
Expand All @@ -100,6 +124,7 @@ func (s *BackupDataSuite) TestValidateProfile(c *C) {
{"Valid Profile", newValidProfile(), IsNil},
{"Valid Profile with Secret Credentials", newValidProfileWithSecretCredentials(), IsNil},
{"Invalid Profile", newInvalidProfile(), NotNil},
{"Invalid Profile with Secret Credentials", newInvalidProfileWithSecretCredentials(), NotNil},
{"Nil Profile", nil, NotNil},
}
for _, tc := range testCases {
Expand Down
11 changes: 3 additions & 8 deletions pkg/function/create_volume_from_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,14 @@ func createVolumeFromSnapshot(ctx context.Context, cli kubernetes.Interface, nam
if len(pvcNames) > 0 {
pvcName = pvcNames[i]
}
config := make(map[string]string)
if err = ValidateProfile(profile, pvcInfo.Type); err != nil {
return nil, errors.Wrap(err, "Profile validation failed")
}
switch pvcInfo.Type {
case blockstorage.TypeEBS:
config := getConfig(profile, pvcInfo.Type)
if pvcInfo.Type == blockstorage.TypeEBS {
config[awsconfig.ConfigRegion] = pvcInfo.Region
config[awsconfig.AccessKeyID] = profile.Credential.KeyPair.ID
config[awsconfig.SecretAccessKey] = profile.Credential.KeyPair.Secret
case blockstorage.TypeGPD:
config[blockstorage.GoogleProjectID] = profile.Credential.KeyPair.ID
config[blockstorage.GoogleServiceKey] = profile.Credential.KeyPair.Secret
}

provider, err := getter.Get(pvcInfo.Type, config)
if err != nil {
return nil, errors.Wrapf(err, "Could not get storage provider %v", pvcInfo.Type)
Expand Down
Loading

0 comments on commit 2733489

Please sign in to comment.