Skip to content

Commit

Permalink
Pass long-term AWS credentials via file (#252)
Browse files Browse the repository at this point in the history
* Add methods for creating and cleaning up AWS Profiles

Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>

* Use credentials and config file to pass long-term credentials

Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>

* Fail if AWS Credentials contains non-printable characters

Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>

---------

Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>
(cherry picked from commit d4c189b)
  • Loading branch information
unexge committed Sep 12, 2024
1 parent 216ca22 commit 8ff6b83
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 74 deletions.
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
toolchain go1.22.4

require (
github.com/aws/aws-sdk-go-v2/config v1.27.33
github.com/container-storage-interface/spec v1.9.0
github.com/godbus/dbus/v5 v5.1.0
github.com/golang/mock v1.6.0
Expand All @@ -18,6 +19,18 @@ require (
)

require (
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
Expand Down
26 changes: 26 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/container-storage-interface/spec v1.9.0 h1:zKtX4STsq31Knz3gciCYCi1SXtO2HJDecIjDVboYavY=
github.com/container-storage-interface/spec v1.9.0/go.mod h1:ZfDu+3ZRyeVqxZM0Ds19MVLkN2d1XJ5MAfi1L3VjlT0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand Down
119 changes: 119 additions & 0 deletions pkg/driver/aws_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package driver

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"unicode"
)

const (
awsProfileName = "s3-csi"
awsProfileConfigFilename = "s3-csi-config"
awsProfileCredentialsFilename = "s3-csi-credentials"
awsProfileFilePerm = fs.FileMode(0400) // only owner readable
)

// ErrInvalidCredentials is returned when given AWS Credentials contains invalid characters.
var ErrInvalidCredentials = errors.New("aws-profile: Invalid AWS Credentials")

// An AWSProfile represents an AWS profile with it's credentials and config files.
type AWSProfile struct {
Name string
ConfigPath string
CredentialsPath string
}

// CreateAWSProfile creates an AWS Profile with credentials and config files from given credentials.
// Created credentials and config files can be clean up with `CleanupAWSProfile`.
func CreateAWSProfile(basepath string, accessKeyID string, secretAccessKey string, sessionToken string) (AWSProfile, error) {
if !isValidCredential(accessKeyID) || !isValidCredential(secretAccessKey) || !isValidCredential(sessionToken) {
return AWSProfile{}, ErrInvalidCredentials
}

name := awsProfileName

configPath := filepath.Join(basepath, awsProfileConfigFilename)
err := writeAWSProfileFile(configPath, configFileContents(name))
if err != nil {
return AWSProfile{}, fmt.Errorf("aws-profile: Failed to create config file %s: %v", configPath, err)
}

credentialsPath := filepath.Join(basepath, awsProfileCredentialsFilename)
err = writeAWSProfileFile(credentialsPath, credentialsFileContents(name, accessKeyID, secretAccessKey, sessionToken))
if err != nil {
return AWSProfile{}, fmt.Errorf("aws-profile: Failed to create credentials file %s: %v", credentialsPath, err)
}

return AWSProfile{
Name: name,
ConfigPath: configPath,
CredentialsPath: credentialsPath,
}, nil
}

// CleanupAWSProfile cleans up credentials and config files created in given `basepath` via `CreateAWSProfile`.
func CleanupAWSProfile(basepath string) error {
configPath := filepath.Join(basepath, awsProfileConfigFilename)
if err := os.Remove(configPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("aws-profile: Failed to remove config file %s: %v", configPath, err)
}
}

credentialsPath := filepath.Join(basepath, awsProfileCredentialsFilename)
if err := os.Remove(credentialsPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("aws-profile: Failed to remove credentials file %s: %v", credentialsPath, err)
}
}

return nil
}

func writeAWSProfileFile(path string, content string) error {
err := os.WriteFile(path, []byte(content), awsProfileFilePerm)
if err != nil {
return err
}
// If the given file exists, `os.WriteFile` just truncates it without changing it's permissions,
// so we need to ensure it always has the correct permissions.
return os.Chmod(path, awsProfileFilePerm)
}

func credentialsFileContents(profile string, accessKeyID string, secretAccessKey string, sessionToken string) string {
var b strings.Builder
b.Grow(128)
b.WriteRune('[')
b.WriteString(profile)
b.WriteRune(']')
b.WriteRune('\n')

b.WriteString("aws_access_key_id=")
b.WriteString(accessKeyID)
b.WriteRune('\n')

b.WriteString("aws_secret_access_key=")
b.WriteString(secretAccessKey)
b.WriteRune('\n')

if sessionToken != "" {
b.WriteString("aws_session_token=")
b.WriteString(sessionToken)
b.WriteRune('\n')
}

return b.String()
}

func configFileContents(profile string) string {
return fmt.Sprintf("[profile %s]\n", profile)
}

// isValidCredential checks whether given credential file contains any non-printable characters.
func isValidCredential(s string) bool {
return !strings.ContainsFunc(s, func(r rune) bool { return !unicode.IsPrint(r) })
}
113 changes: 113 additions & 0 deletions pkg/driver/aws_profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package driver_test

import (
"context"
"errors"
"io/fs"
"os"
"testing"

"github.com/awslabs/aws-s3-csi-driver/pkg/driver"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
)

const testAccessKeyId = "test-access-key-id"
const testSecretAccessKey = "test-secret-access-key"
const testSessionToken = "test-session-token"

func TestCreatingAWSProfile(t *testing.T) {
t.Run("create config and credentials files", func(t *testing.T) {
profile, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken)
assertNoError(t, err)
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, testSessionToken)
})

t.Run("create config and credentials files with empty session token", func(t *testing.T) {
profile, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, "")
assertNoError(t, err)
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, "")
})

t.Run("ensure config and credentials files are owner readable only", func(t *testing.T) {
profile, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken)
assertNoError(t, err)
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, testSessionToken)

configStat, err := os.Stat(profile.ConfigPath)
assertNoError(t, err)
assertEquals(t, 0400, configStat.Mode())

credentialsStat, err := os.Stat(profile.CredentialsPath)
assertNoError(t, err)
assertEquals(t, 0400, credentialsStat.Mode())
})

t.Run("fail if credentials contains non-ascii characters", func(t *testing.T) {
t.Run("access key ID", func(t *testing.T) {
_, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId+"\n\t\r credential_process=exit", testSecretAccessKey, testSessionToken)
assertEquals(t, true, errors.Is(err, driver.ErrInvalidCredentials))
})
t.Run("secret access key", func(t *testing.T) {
_, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey+"\n", testSessionToken)
assertEquals(t, true, errors.Is(err, driver.ErrInvalidCredentials))
})
t.Run("session token", func(t *testing.T) {
_, err := driver.CreateAWSProfile(t.TempDir(), testAccessKeyId, testSecretAccessKey, testSessionToken+"\n\r")
assertEquals(t, true, errors.Is(err, driver.ErrInvalidCredentials))
})
})
}

func TestCleaningUpAWSProfile(t *testing.T) {
t.Run("clean config and credentials files", func(t *testing.T) {
basepath := t.TempDir()

profile, err := driver.CreateAWSProfile(basepath, testAccessKeyId, testSecretAccessKey, testSessionToken)
assertNoError(t, err)
assertCredentialsFromAWSProfile(t, profile, testAccessKeyId, testSecretAccessKey, testSessionToken)

err = driver.CleanupAWSProfile(basepath)
assertNoError(t, err)

_, err = os.Stat(profile.ConfigPath)
assertEquals(t, true, errors.Is(err, fs.ErrNotExist))

_, err = os.Stat(profile.CredentialsPath)
assertEquals(t, true, errors.Is(err, fs.ErrNotExist))
})

t.Run("cleaning non-existent config and credentials files should not be an error", func(t *testing.T) {
err := driver.CleanupAWSProfile(t.TempDir())
assertNoError(t, err)
})
}

func assertCredentialsFromAWSProfile(t *testing.T, profile driver.AWSProfile, accessKeyID string, secretAccessKey string, sessionToken string) {
credentials := parseAWSProfile(t, profile)
assertEquals(t, accessKeyID, credentials.AccessKeyID)
assertEquals(t, secretAccessKey, credentials.SecretAccessKey)
assertEquals(t, sessionToken, credentials.SessionToken)
}

func parseAWSProfile(t *testing.T, profile driver.AWSProfile) aws.Credentials {
sharedConfig, err := config.LoadSharedConfigProfile(context.Background(), profile.Name, func(c *config.LoadSharedConfigOptions) {
c.ConfigFiles = []string{profile.ConfigPath}
c.CredentialsFiles = []string{profile.CredentialsPath}
})
assertNoError(t, err)
return sharedConfig.Credentials
}

func assertEquals[T comparable](t *testing.T, expected T, got T) {
if expected != got {
t.Errorf("Expected %#v, Got %#v", expected, got)
}
}

func assertNoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Expected no error, but got: %s", err)
}
}
Loading

0 comments on commit 8ff6b83

Please # to comment.