From 9d2957c65714886ab22632712abf490f1e31adf0 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 28 Nov 2024 10:52:08 +0000 Subject: [PATCH 1/9] Extract shared/common flags to struct Signed-off-by: Noah Stride --- cmd/main.go | 108 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 36eb522..4eea625 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -50,19 +50,77 @@ func newRootCmd() (*cobra.Command, error) { } rootCmd.AddCommand(x509CredentialProcessCmd) + oneShotCredentialWriteCmd, err := newOneShotCredentialWrite() + if err != nil { + return nil, fmt.Errorf("initializing one-shot-credential-write command: %w", err) + } + rootCmd.AddCommand(oneShotCredentialWriteCmd) + return rootCmd, nil } +type sharedFlags struct { + roleARN string + region string + profileARN string + sessionDuration int + trustAnchorARN string + roleSessionName string + workloadAPIAddr string +} + +func (f *sharedFlags) addFlags(cmd *cobra.Command) error { + cmd.Flags().StringVar(&f.roleARN, "role-arn", "", "The ARN of the role to assume. Required.") + if err := cmd.MarkFlagRequired("role-arn"); err != nil { + return fmt.Errorf("marking role-arn flag as required: %w", err) + } + cmd.Flags().StringVar(&f.region, "region", "", "Overrides AWS region to use when exchanging the SVID for AWS credentials. Optional.") + cmd.Flags().StringVar(&f.profileARN, "profile-arn", "", "The ARN of the Roles Anywhere profile to use. Required.") + if err := cmd.MarkFlagRequired("profile-arn"); err != nil { + return fmt.Errorf("marking profile-arn flag as required: %w", err) + } + cmd.Flags().IntVar(&f.sessionDuration, "session-duration", 3600, "The duration, in seconds, of the resulting session. Optional. Can range from 15 minutes (900) to 12 hours (43200).") + cmd.Flags().StringVar(&f.trustAnchorARN, "trust-anchor-arn", "", "The ARN of the Roles Anywhere trust anchor to use. Required.") + if err := cmd.MarkFlagRequired("trust-anchor-arn"); err != nil { + return fmt.Errorf("marking trust-anchor-arn flag as required: %w", err) + } + cmd.Flags().StringVar(&f.roleSessionName, "role-session-name", "", "The identifier for the role session. Optional.") + cmd.Flags().StringVar(&f.workloadAPIAddr, "workload-api-addr", "", "Overrides the address of the Workload API endpoint that will be use to fetch the X509 SVID. If unspecified, the value from the SPIFFE_ENDPOINT_SOCKET environment variable will be used.") + return nil +} + +func newOneShotCredentialWrite() (*cobra.Command, error) { + sf := &sharedFlags{} + cmd := &cobra.Command{ + Use: "x509-one-shot-credential-write", + Short: ``, + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + + x509Ctx, err := client.FetchX509Context(ctx) + if err != nil { + return fmt.Errorf("fetching x509 context: %w", err) + } + }, + // Hidden for now as the daemon is likely more "usable" + Hidden: true, + } + if err := sf.addFlags(cmd); err != nil { + return nil, fmt.Errorf("adding shared flags: %w", err) + } + return cmd, nil +} + func newX509CredentialProcessCmd() (*cobra.Command, error) { - var ( - roleARN string - region string - profileARN string - sessionDuration int - trustAnchorARN string - roleSessionName string - workloadAPIAddr string - ) + sf := &sharedFlags{} cmd := &cobra.Command{ Use: "x509-credential-process", Short: `Exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Compatible with the AWS credential process functionality.`, @@ -71,7 +129,7 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { ctx := cmd.Context() client, err := workloadapi.New( ctx, - workloadapi.WithAddr(workloadAPIAddr), + workloadapi.WithAddr(sf.workloadAPIAddr), ) if err != nil { return fmt.Errorf("creating workload api client: %w", err) @@ -100,12 +158,12 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { return fmt.Errorf("getting signature algorithm: %w", err) } credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ - RoleArn: roleARN, - ProfileArnStr: profileARN, - Region: region, - RoleSessionName: roleSessionName, - TrustAnchorArnStr: trustAnchorARN, - SessionDuration: sessionDuration, + RoleArn: sf.roleARN, + ProfileArnStr: sf.profileARN, + Region: sf.region, + RoleSessionName: sf.roleSessionName, + TrustAnchorArnStr: sf.trustAnchorARN, + SessionDuration: sf.sessionDuration, }, signer, signatureAlgorithm) if err != nil { return fmt.Errorf("generating credentials: %w", err) @@ -126,21 +184,9 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { return nil }, } - cmd.Flags().StringVar(&roleARN, "role-arn", "", "The ARN of the role to assume. Required.") - if err := cmd.MarkFlagRequired("role-arn"); err != nil { - return nil, fmt.Errorf("marking role-arn flag as required: %w", err) + if err := sf.addFlags(cmd); err != nil { + return nil, fmt.Errorf("adding shared flags: %w", err) } - cmd.Flags().StringVar(®ion, "region", "", "Overrides AWS region to use when exchanging the SVID for AWS credentials. Optional.") - cmd.Flags().StringVar(&profileARN, "profile-arn", "", "The ARN of the Roles Anywhere profile to use. Required.") - if err := cmd.MarkFlagRequired("profile-arn"); err != nil { - return nil, fmt.Errorf("marking profile-arn flag as required: %w", err) - } - cmd.Flags().IntVar(&sessionDuration, "session-duration", 3600, "The duration, in seconds, of the resulting session. Optional. Can range from 15 minutes (900) to 12 hours (43200).") - cmd.Flags().StringVar(&trustAnchorARN, "trust-anchor-arn", "", "The ARN of the Roles Anywhere trust anchor to use. Required.") - if err := cmd.MarkFlagRequired("trust-anchor-arn"); err != nil { - return nil, fmt.Errorf("marking trust-anchor-arn flag as required: %w", err) - } - cmd.Flags().StringVar(&roleSessionName, "role-session-name", "", "The identifier for the role session. Optional.") - cmd.Flags().StringVar(&workloadAPIAddr, "workload-api-addr", "", "Overrides the address of the Workload API endpoint that will be use to fetch the X509 SVID. If unspecified, the value from the SPIFFE_ENDPOINT_SOCKET environment variable will be used.") + return cmd, nil } From c14871131aa74fe768226a19c49f98bfd7116e03 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 28 Nov 2024 11:36:24 +0000 Subject: [PATCH 2/9] Start hacking on AWS credentials file compatible mode Signed-off-by: Noah Stride --- cmd/main.go | 42 +++++++++++++-- go.mod | 6 +++ go.sum | 4 ++ internal/aws_credentials_file.go | 74 +++++++++++++++++++++++++++ internal/aws_credentials_file_test.go | 69 +++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 internal/aws_credentials_file.go create mode 100644 internal/aws_credentials_file_test.go diff --git a/cmd/main.go b/cmd/main.go index 4eea625..bea3fae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -50,9 +50,9 @@ func newRootCmd() (*cobra.Command, error) { } rootCmd.AddCommand(x509CredentialProcessCmd) - oneShotCredentialWriteCmd, err := newOneShotCredentialWrite() + oneShotCredentialWriteCmd, err := newX509CredentialFileCmd() if err != nil { - return nil, fmt.Errorf("initializing one-shot-credential-write command: %w", err) + return nil, fmt.Errorf("initializing x509-credential-file command: %w", err) } rootCmd.AddCommand(oneShotCredentialWriteCmd) @@ -89,10 +89,10 @@ func (f *sharedFlags) addFlags(cmd *cobra.Command) error { return nil } -func newOneShotCredentialWrite() (*cobra.Command, error) { +func newX509CredentialFileCmd() (*cobra.Command, error) { sf := &sharedFlags{} cmd := &cobra.Command{ - Use: "x509-one-shot-credential-write", + Use: "x509-credential-file", Short: ``, Long: ``, RunE: func(cmd *cobra.Command, args []string) error { @@ -109,6 +109,40 @@ func newOneShotCredentialWrite() (*cobra.Command, error) { if err != nil { return fmt.Errorf("fetching x509 context: %w", err) } + svid := x509Ctx.DefaultSVID() + slog.Debug( + "Fetched X509 SVID", + slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + ), + ) + + signer := &awsspiffe.X509SVIDSigner{ + SVID: svid, + } + signatureAlgorithm, err := signer.SignatureAlgorithm() + if err != nil { + return fmt.Errorf("getting signature algorithm: %w", err) + } + credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ + RoleArn: sf.roleARN, + ProfileArnStr: sf.profileARN, + Region: sf.region, + RoleSessionName: sf.roleSessionName, + TrustAnchorArnStr: sf.trustAnchorARN, + SessionDuration: sf.sessionDuration, + }, signer, signatureAlgorithm) + if err != nil { + return fmt.Errorf("generating credentials: %w", err) + } + slog.Debug( + "Generated AWS credentials", + "expiration", credentials.Expiration, + ) + + // Now we write this to disk in the format that the AWS CLI/SDK + // expects for a credentials file. }, // Hidden for now as the daemon is likely more "usable" Hidden: true, diff --git a/go.mod b/go.mod index 50f3d03..c1a96f7 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,16 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/zeebo/errs v1.3.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect @@ -26,4 +30,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 06adbdd..390510f 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= @@ -53,6 +55,8 @@ google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFN google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/aws_credentials_file.go b/internal/aws_credentials_file.go new file mode 100644 index 0000000..19bd903 --- /dev/null +++ b/internal/aws_credentials_file.go @@ -0,0 +1,74 @@ +package internal + +import ( + "fmt" + "log/slog" + "os" + + "gopkg.in/ini.v1" +) + +type AWSCredentialsFileConfig struct { + Path string + ProfileName string + Force bool + ReplaceFile bool +} + +type AWSCredentialsFileProfile struct { + AWSAccessKeyID string + AWSSecretAccessKey string + AWSSessionToken string +} + +func (p AWSCredentialsFileProfile) Validate() error { + // TODO: Validate + return nil +} + +// UpsertAWSCredentialsFileProfile writes the provided AWS credentials profile to the AWS credentials file. +// See https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html +func UpsertAWSCredentialsFileProfile( + log *slog.Logger, + cfg AWSCredentialsFileConfig, + p AWSCredentialsFileProfile, +) error { + if err := p.Validate(); err != nil { + return fmt.Errorf("validating aws credentials file profile: %w", err) + } + + f, err := ini.Load(cfg.Path) + if err != nil { + if !os.IsNotExist(err) { + if !cfg.Force { + log.Error( + "When loading the existing AWS credentials file, an error occurred. Use --force to ignore errors and attempt to overwrite.", + "error", err, + "path", cfg.Path, + ) + return fmt.Errorf("loading existing aws credentials file: %w", err) + } + log.Warn( + "When loading the existing AWS credentials file, an error occurred. As --force is set, the file will be overwritten.", + "error", err, + "path", cfg.Path, + ) + } + f = ini.Empty() + } + + sectionName := "default" + if cfg.ProfileName != "" { + sectionName = cfg.ProfileName + } + sec := f.Section(sectionName) + + sec.Key("aws_secret_access_key").SetValue(p.AWSSecretAccessKey) + sec.Key("aws_access_key_id").SetValue(p.AWSAccessKeyID) + sec.Key("aws_session_token").SetValue(p.AWSSessionToken) + + if err := f.SaveTo(cfg.Path); err != nil { + return fmt.Errorf("saving aws credentials file: %w", err) + } + return nil +} diff --git a/internal/aws_credentials_file_test.go b/internal/aws_credentials_file_test.go new file mode 100644 index 0000000..df40a96 --- /dev/null +++ b/internal/aws_credentials_file_test.go @@ -0,0 +1,69 @@ +package internal + +import ( + "log/slog" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAWSCredentialsFile_Write(t *testing.T) { + // TODO: Add more cases: + // - If file exists, but is a bad ini and Force moe + // - Replace mode + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config") + log := slog.Default() + + err := UpsertAWSCredentialsFileProfile( + log, + AWSCredentialsFileConfig{ + Path: configPath, + }, + AWSCredentialsFileProfile{ + AWSAccessKeyID: "1234567890", + AWSSecretAccessKey: "abcdefgh", + AWSSessionToken: "ijklmnop", + }, + ) + require.NoError(t, err) + + got, err := os.ReadFile(configPath) + require.NoError(t, err) + + require.Equal(t, `[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`, string(got)) + + t.Run("bad file", func(t *testing.T) { + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config") + require.NoError(t, os.WriteFile(configPath, []byte("bad ini"), 0600)) + err := UpsertAWSCredentialsFileProfile( + log, + AWSCredentialsFileConfig{ + Path: configPath, + Force: true, + }, + AWSCredentialsFileProfile{ + AWSAccessKeyID: "1234567890", + AWSSecretAccessKey: "abcdefgh", + AWSSessionToken: "ijklmnop", + }, + ) + require.NoError(t, err) + + got, err := os.ReadFile(configPath) + require.NoError(t, err) + + require.Equal(t, `[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`, string(got)) + }) +} From 9f040cea70653104ea071cf8d8f98a597b63b9df Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 28 Nov 2024 13:38:18 +0000 Subject: [PATCH 3/9] Refactor core logic into exchangeX509SVIDForAWSCredentials and add flags Signed-off-by: Noah Stride --- cmd/main.go | 105 +++++++++++++++++++------------ internal/aws_credentials_file.go | 62 +++++++++++------- 2 files changed, 103 insertions(+), 64 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index bea3fae..a083835 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,7 +8,9 @@ import ( "github.com/spf13/cobra" awsspiffe "github.com/spiffe/aws-spiffe-workload-helper" + "github.com/spiffe/aws-spiffe-workload-helper/internal" "github.com/spiffe/aws-spiffe-workload-helper/internal/vendoredaws" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) @@ -89,7 +91,40 @@ func (f *sharedFlags) addFlags(cmd *cobra.Command) error { return nil } +func exchangeX509SVIDForAWSCredentials( + sf *sharedFlags, + svid *x509svid.SVID, +) (vendoredaws.CredentialProcessOutput, error) { + signer := &awsspiffe.X509SVIDSigner{ + SVID: svid, + } + signatureAlgorithm, err := signer.SignatureAlgorithm() + if err != nil { + return vendoredaws.CredentialProcessOutput{}, fmt.Errorf("getting signature algorithm: %w", err) + } + credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ + RoleArn: sf.roleARN, + ProfileArnStr: sf.profileARN, + Region: sf.region, + RoleSessionName: sf.roleSessionName, + TrustAnchorArnStr: sf.trustAnchorARN, + SessionDuration: sf.sessionDuration, + }, signer, signatureAlgorithm) + if err != nil { + return vendoredaws.CredentialProcessOutput{}, fmt.Errorf("generating credentials: %w", err) + } + slog.Debug( + "Generated AWS credentials", + "expiration", credentials.Expiration, + ) + return credentials, nil +} + func newX509CredentialFileCmd() (*cobra.Command, error) { + oneshot := false + force := false + replace := false + awsCredentialsPath := "" sf := &sharedFlags{} cmd := &cobra.Command{ Use: "x509-credential-file", @@ -118,31 +153,31 @@ func newX509CredentialFileCmd() (*cobra.Command, error) { ), ) - signer := &awsspiffe.X509SVIDSigner{ - SVID: svid, - } - signatureAlgorithm, err := signer.SignatureAlgorithm() - if err != nil { - return fmt.Errorf("getting signature algorithm: %w", err) - } - credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ - RoleArn: sf.roleARN, - ProfileArnStr: sf.profileARN, - Region: sf.region, - RoleSessionName: sf.roleSessionName, - TrustAnchorArnStr: sf.trustAnchorARN, - SessionDuration: sf.sessionDuration, - }, signer, signatureAlgorithm) + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) if err != nil { - return fmt.Errorf("generating credentials: %w", err) + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) } - slog.Debug( - "Generated AWS credentials", - "expiration", credentials.Expiration, - ) // Now we write this to disk in the format that the AWS CLI/SDK // expects for a credentials file. + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credential to file", "path", "./my-credential") + return nil }, // Hidden for now as the daemon is likely more "usable" Hidden: true, @@ -150,6 +185,14 @@ func newX509CredentialFileCmd() (*cobra.Command, error) { if err := sf.addFlags(cmd); err != nil { return nil, fmt.Errorf("adding shared flags: %w", err) } + cmd.Flags().BoolVar(&oneshot, "oneshot", false, "If set, generate the AWS credentials file and exit. If unset, run as a daemon that continuously updates the file before expiry or when a new SVID is available.") + cmd.Flags().StringVar(&awsCredentialsPath, "aws-credentials-path", "", "The path to the AWS credentials file to write.") + if err := cmd.MarkFlagRequired("aws-credentials-path"); err != nil { + return nil, fmt.Errorf("marking aws-credentials-path flag as required: %w", err) + } + cmd.Flags().BoolVar(&force, "force", false, "If set, failures loading the existing AWS credentials file will be ignored and the contents overwritten.") + cmd.Flags().BoolVar(&replace, "replace", false, "If set, the AWS credentials file will be replaced if it exists. This will remove any profiles not written by this tool.") + return cmd, nil } @@ -184,28 +227,10 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { ), ) - signer := &awsspiffe.X509SVIDSigner{ - SVID: svid, - } - signatureAlgorithm, err := signer.SignatureAlgorithm() - if err != nil { - return fmt.Errorf("getting signature algorithm: %w", err) - } - credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ - RoleArn: sf.roleARN, - ProfileArnStr: sf.profileARN, - Region: sf.region, - RoleSessionName: sf.roleSessionName, - TrustAnchorArnStr: sf.trustAnchorARN, - SessionDuration: sf.sessionDuration, - }, signer, signatureAlgorithm) + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) if err != nil { - return fmt.Errorf("generating credentials: %w", err) + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) } - slog.Debug( - "Generated AWS credentials", - "expiration", credentials.Expiration, - ) out, err := json.Marshal(credentials) if err != nil { diff --git a/internal/aws_credentials_file.go b/internal/aws_credentials_file.go index 19bd903..7f1c1c1 100644 --- a/internal/aws_credentials_file.go +++ b/internal/aws_credentials_file.go @@ -21,9 +21,42 @@ type AWSCredentialsFileProfile struct { AWSSessionToken string } -func (p AWSCredentialsFileProfile) Validate() error { - // TODO: Validate - return nil +func loadAWSCredentialsFile( + log *slog.Logger, + cfg AWSCredentialsFileConfig, +) (*ini.File, error) { + if cfg.ReplaceFile { + return ini.Empty(), nil + } + + f, err := ini.Load(cfg.Path) + if err == nil { + return f, nil + } + + // If it doesn't exist, we can "create" it. + // TODO: Make directory/parent directories if necessary. + if os.IsNotExist(err) { + return ini.Empty(), nil + } + + // If force mode is enabled, ignore the error and return an empty file. + if cfg.Force { + log.Warn( + "When loading the existing AWS credentials file, an error occurred. As --force is set, the file will be overwritten.", + "error", err, + "path", cfg.Path, + ) + return ini.Empty(), nil + } + + // Otherwise, fail... + log.Error( + "When loading the existing AWS credentials file, an error occurred. Use --force to ignore errors and attempt to overwrite.", + "error", err, + "path", cfg.Path, + ) + return nil, fmt.Errorf("loading existing aws credentials file: %w", err) } // UpsertAWSCredentialsFileProfile writes the provided AWS credentials profile to the AWS credentials file. @@ -33,28 +66,9 @@ func UpsertAWSCredentialsFileProfile( cfg AWSCredentialsFileConfig, p AWSCredentialsFileProfile, ) error { - if err := p.Validate(); err != nil { - return fmt.Errorf("validating aws credentials file profile: %w", err) - } - - f, err := ini.Load(cfg.Path) + f, err := loadAWSCredentialsFile(log, cfg) if err != nil { - if !os.IsNotExist(err) { - if !cfg.Force { - log.Error( - "When loading the existing AWS credentials file, an error occurred. Use --force to ignore errors and attempt to overwrite.", - "error", err, - "path", cfg.Path, - ) - return fmt.Errorf("loading existing aws credentials file: %w", err) - } - log.Warn( - "When loading the existing AWS credentials file, an error occurred. As --force is set, the file will be overwritten.", - "error", err, - "path", cfg.Path, - ) - } - f = ini.Empty() + return fmt.Errorf("loading existing aws credentials: %w", err) } sectionName := "default" From 4ca067924b9c1983cd6ee53c75b3abc06f35b88b Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 28 Nov 2024 15:59:51 +0000 Subject: [PATCH 4/9] Add tests for force/replace mode Signed-off-by: Noah Stride --- internal/aws_credentials_file_test.go | 165 +++++++++++++++++++------- 1 file changed, 122 insertions(+), 43 deletions(-) diff --git a/internal/aws_credentials_file_test.go b/internal/aws_credentials_file_test.go index df40a96..940f5af 100644 --- a/internal/aws_credentials_file_test.go +++ b/internal/aws_credentials_file_test.go @@ -10,60 +10,139 @@ import ( ) func TestAWSCredentialsFile_Write(t *testing.T) { - // TODO: Add more cases: - // - If file exists, but is a bad ini and Force moe - // - Replace mode - tmp := t.TempDir() - configPath := filepath.Join(tmp, "config") log := slog.Default() - err := UpsertAWSCredentialsFileProfile( - log, - AWSCredentialsFileConfig{ - Path: configPath, - }, - AWSCredentialsFileProfile{ - AWSAccessKeyID: "1234567890", - AWSSecretAccessKey: "abcdefgh", - AWSSessionToken: "ijklmnop", - }, - ) - require.NoError(t, err) + defaultProfile := AWSCredentialsFileProfile{ + AWSAccessKeyID: "1234567890", + AWSSecretAccessKey: "abcdefgh", + AWSSessionToken: "ijklmnop", + } - got, err := os.ReadFile(configPath) - require.NoError(t, err) + preExistingContents := []byte(`[pre-existing] +aws_secret_access_key = foo +aws_access_key_id = bar +aws_session_token = bizz +`) - require.Equal(t, `[default] + tests := []struct { + name string + existingFileContents []byte + config AWSCredentialsFileConfig + profile AWSCredentialsFileProfile + want []byte + wantErr string + }{ + { + name: "no pre-existing file - default profile", + config: AWSCredentialsFileConfig{}, + profile: defaultProfile, + want: []byte(`[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`), + }, + { + name: "no pre-existing file - named profile", + config: AWSCredentialsFileConfig{ + ProfileName: "my-profile", + }, + profile: defaultProfile, + want: []byte(`[my-profile] aws_secret_access_key = abcdefgh aws_access_key_id = 1234567890 aws_session_token = ijklmnop -`, string(got)) +`), + }, + { + name: "pre-existing file, no profile name clash - default profile", + config: AWSCredentialsFileConfig{}, + profile: defaultProfile, + existingFileContents: preExistingContents, + want: []byte(`[pre-existing] +aws_secret_access_key = foo +aws_access_key_id = bar +aws_session_token = bizz - t.Run("bad file", func(t *testing.T) { - tmp := t.TempDir() - configPath := filepath.Join(tmp, "config") - require.NoError(t, os.WriteFile(configPath, []byte("bad ini"), 0600)) - err := UpsertAWSCredentialsFileProfile( - log, - AWSCredentialsFileConfig{ - Path: configPath, - Force: true, +[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`), + }, + { + name: "pre-existing file, no profile name clash - default profile with replace mode", + config: AWSCredentialsFileConfig{ + ReplaceFile: true, }, - AWSCredentialsFileProfile{ - AWSAccessKeyID: "1234567890", - AWSSecretAccessKey: "abcdefgh", - AWSSessionToken: "ijklmnop", + profile: defaultProfile, + existingFileContents: preExistingContents, + want: []byte(`[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`), + }, + { + name: "pre-existing file, profile name clash", + config: AWSCredentialsFileConfig{ + ProfileName: "pre-existing", }, - ) - require.NoError(t, err) - - got, err := os.ReadFile(configPath) - require.NoError(t, err) - - require.Equal(t, `[default] + profile: defaultProfile, + existingFileContents: preExistingContents, + want: []byte(`[pre-existing] aws_secret_access_key = abcdefgh aws_access_key_id = 1234567890 aws_session_token = ijklmnop -`, string(got)) - }) +`), + }, + { + name: "pre-existing file with garbage", + config: AWSCredentialsFileConfig{}, + profile: defaultProfile, + existingFileContents: []byte(`dduhufd`), + wantErr: "key-value delimiter not found", + }, + { + name: "pre-existing file with garbage, --force", + config: AWSCredentialsFileConfig{ + Force: true, + }, + profile: defaultProfile, + existingFileContents: []byte(`dduhufd`), + want: []byte(`[default] +aws_secret_access_key = abcdefgh +aws_access_key_id = 1234567890 +aws_session_token = ijklmnop +`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmp := t.TempDir() + credentialPath := filepath.Join(tmp, "credentials") + cfg := tt.config + cfg.Path = credentialPath + + if tt.existingFileContents != nil { + require.NoError(t, os.WriteFile(credentialPath, tt.existingFileContents, 0600)) + } + + err := UpsertAWSCredentialsFileProfile( + log, + cfg, + tt.profile, + ) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + got, err := os.ReadFile(credentialPath) + require.NoError(t, err) + + require.Equal(t, string(tt.want), string(got)) + }) + } } From 022905d224800b0f9a8ae7373b06941ccd0c63c8 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Thu, 28 Nov 2024 17:40:25 +0000 Subject: [PATCH 5/9] Add basic daemon mode loop Signed-off-by: Noah Stride --- cmd/main.go | 228 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 183 insertions(+), 45 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index a083835..c487979 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,10 +1,12 @@ package main import ( + "context" "encoding/json" "fmt" "log/slog" "os" + "time" "github.com/spf13/cobra" awsspiffe "github.com/spiffe/aws-spiffe-workload-helper" @@ -120,6 +122,176 @@ func exchangeX509SVIDForAWSCredentials( return credentials, nil } +func oneshotX509CredentialFile( + ctx context.Context, + force bool, + replace bool, + awsCredentialsPath string, + sf *sharedFlags, +) error { + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + x509Ctx, err := client.FetchX509Context(ctx) + if err != nil { + return fmt.Errorf("fetching x509 context: %w", err) + } + svid := x509Ctx.DefaultSVID() + slog.Debug( + "Fetched X509 SVID", + slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + ), + ) + + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + + // Now we write this to disk in the format that the AWS CLI/SDK + // expects for a credentials file. + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credential to file", "path", "./my-credential") + return nil +} + +func daemonX509CredentialFile( + ctx context.Context, + force bool, + replace bool, + awsCredentialsPath string, + sf *sharedFlags, +) error { + slog.Info("Starting AWS credential file daemon") + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + slog.Debug("Fetching initial X509 SVID") + x509Source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClient(client)) + if err != nil { + return fmt.Errorf("creating x509 source: %w", err) + } + defer func() { + if err := x509Source.Close(); err != nil { + slog.Warn("Failed to close x509 source", "error", err) + } + }() + + svidUpdate := x509Source.Updated() + svid, err := x509Source.GetX509SVID() + if err != nil { + return fmt.Errorf("fetching initial X509 SVID: %w", err) + } + slog.Debug("Fetched initial X509 SVID", slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + "expires_at", svid.Certificates[0].NotAfter, + )) + + for { + slog.Debug("Exchanging X509 SVID for AWS credentials") + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + slog.Info( + "Successfully exchanged X509 SVID for AWS credentials", + ) + + expiresAt, err := time.Parse(time.RFC3339, credentials.Expiration) + if err != nil { + return fmt.Errorf("parsing expiration time: %w", err) + } + + slog.Debug("Writing AWS credentials to file", "path", awsCredentialsPath) + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credentials to file", "path", awsCredentialsPath) + + slog.Info( + "Sleeping until a new X509 SVID is received or the AWS credentials are close to expiry", + "aws_expires_at", expiresAt, + "aws_ttl", expiresAt.Sub(time.Now()), + "svid_expires_at", svid.Certificates[0].NotAfter, + "svid_ttl", svid.Certificates[0].NotAfter.Sub(time.Now()), + ) + select { + case <-time.After(time.Second * 10): + slog.Info("Triggering renewal as AWS credentials are close to expiry") + // TODO: Add case for AWS credential approaching expiry + case <-svidUpdate: + slog.Debug("Received potential X509 SVID update") + newSVID, err := x509Source.GetX509SVID() + if err != nil { + return fmt.Errorf("fetching updated X509 SVID: %w", err) + } + slog.Info( + "Received new X509 SVID from Workload API, will update AWS credentials", + slog.Group("svid", + "spiffe_id", newSVID.ID, + "hint", newSVID.Hint, + "expires_at", newSVID.Certificates[0].NotAfter, + ), + ) + svid = newSVID + case <-ctx.Done(): + return nil + } + } + +} + func newX509CredentialFileCmd() (*cobra.Command, error) { oneshot := false force := false @@ -131,53 +303,14 @@ func newX509CredentialFileCmd() (*cobra.Command, error) { Short: ``, Long: ``, RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := workloadapi.New( - ctx, - workloadapi.WithAddr(sf.workloadAPIAddr), - ) - if err != nil { - return fmt.Errorf("creating workload api client: %w", err) - } - - x509Ctx, err := client.FetchX509Context(ctx) - if err != nil { - return fmt.Errorf("fetching x509 context: %w", err) - } - svid := x509Ctx.DefaultSVID() - slog.Debug( - "Fetched X509 SVID", - slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - ), - ) - - credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) - if err != nil { - return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + if oneshot { + return oneshotX509CredentialFile( + cmd.Context(), force, replace, awsCredentialsPath, sf, + ) } - - // Now we write this to disk in the format that the AWS CLI/SDK - // expects for a credentials file. - err = internal.UpsertAWSCredentialsFileProfile( - slog.Default(), - internal.AWSCredentialsFileConfig{ - Path: awsCredentialsPath, - Force: force, - ReplaceFile: replace, - }, - internal.AWSCredentialsFileProfile{ - AWSAccessKeyID: credentials.AccessKeyId, - AWSSecretAccessKey: credentials.SecretAccessKey, - AWSSessionToken: credentials.SessionToken, - }, + return daemonX509CredentialFile( + cmd.Context(), force, replace, awsCredentialsPath, sf, ) - if err != nil { - return fmt.Errorf("writing credentials to file: %w", err) - } - slog.Info("Wrote AWS credential to file", "path", "./my-credential") - return nil }, // Hidden for now as the daemon is likely more "usable" Hidden: true, @@ -211,6 +344,11 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { if err != nil { return fmt.Errorf("creating workload api client: %w", err) } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() x509Ctx, err := client.FetchX509Context(ctx) if err != nil { From 26b977c50971e88dec0699c7d35486cd2d1aa2e3 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Mon, 2 Dec 2024 10:20:20 +0000 Subject: [PATCH 6/9] Split commands into seperate files Signed-off-by: Noah Stride --- cmd/credential_file.go | 239 ++++++++++++++++++++++++++++++++ cmd/credential_process.go | 70 ++++++++++ cmd/main.go | 281 ++------------------------------------ 3 files changed, 317 insertions(+), 273 deletions(-) create mode 100644 cmd/credential_file.go create mode 100644 cmd/credential_process.go diff --git a/cmd/credential_file.go b/cmd/credential_file.go new file mode 100644 index 0000000..c25b42d --- /dev/null +++ b/cmd/credential_file.go @@ -0,0 +1,239 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/spf13/cobra" + "github.com/spiffe/aws-spiffe-workload-helper/internal" + "github.com/spiffe/go-spiffe/v2/workloadapi" +) + +func newX509CredentialFileOneshotCmd() (*cobra.Command, error) { + force := false + replace := false + awsCredentialsPath := "" + sf := &sharedFlags{} + cmd := &cobra.Command{ + Use: "x509-credential-file-oneshot", + Short: ``, + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + return oneshotX509CredentialFile( + cmd.Context(), force, replace, awsCredentialsPath, sf, + ) + }, + } + if err := sf.addFlags(cmd); err != nil { + return nil, fmt.Errorf("adding shared flags: %w", err) + } + cmd.Flags().StringVar(&awsCredentialsPath, "aws-credentials-path", "", "The path to the AWS credentials file to write.") + if err := cmd.MarkFlagRequired("aws-credentials-path"); err != nil { + return nil, fmt.Errorf("marking aws-credentials-path flag as required: %w", err) + } + cmd.Flags().BoolVar(&force, "force", false, "If set, failures loading the existing AWS credentials file will be ignored and the contents overwritten.") + cmd.Flags().BoolVar(&replace, "replace", false, "If set, the AWS credentials file will be replaced if it exists. This will remove any profiles not written by this tool.") + + return cmd, nil +} + +func oneshotX509CredentialFile( + ctx context.Context, + force bool, + replace bool, + awsCredentialsPath string, + sf *sharedFlags, +) error { + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + x509Ctx, err := client.FetchX509Context(ctx) + if err != nil { + return fmt.Errorf("fetching x509 context: %w", err) + } + svid := x509Ctx.DefaultSVID() + slog.Debug( + "Fetched X509 SVID", + slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + ), + ) + + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + + // Now we write this to disk in the format that the AWS CLI/SDK + // expects for a credentials file. + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credential to file", "path", "./my-credential") + return nil +} + +func newX509CredentialFileCmd() (*cobra.Command, error) { + force := false + replace := false + awsCredentialsPath := "" + sf := &sharedFlags{} + cmd := &cobra.Command{ + Use: "x509-credential-file", + Short: ``, + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + return daemonX509CredentialFile( + cmd.Context(), force, replace, awsCredentialsPath, sf, + ) + }, + // Hidden for now as the daemon is likely more "usable" + Hidden: true, + } + if err := sf.addFlags(cmd); err != nil { + return nil, fmt.Errorf("adding shared flags: %w", err) + } + cmd.Flags().StringVar(&awsCredentialsPath, "aws-credentials-path", "", "The path to the AWS credentials file to write.") + if err := cmd.MarkFlagRequired("aws-credentials-path"); err != nil { + return nil, fmt.Errorf("marking aws-credentials-path flag as required: %w", err) + } + cmd.Flags().BoolVar(&force, "force", false, "If set, failures loading the existing AWS credentials file will be ignored and the contents overwritten.") + cmd.Flags().BoolVar(&replace, "replace", false, "If set, the AWS credentials file will be replaced if it exists. This will remove any profiles not written by this tool.") + + return cmd, nil +} + +func daemonX509CredentialFile( + ctx context.Context, + force bool, + replace bool, + awsCredentialsPath string, + sf *sharedFlags, +) error { + slog.Info("Starting AWS credential file daemon") + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + slog.Debug("Fetching initial X509 SVID") + x509Source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClient(client)) + if err != nil { + return fmt.Errorf("creating x509 source: %w", err) + } + defer func() { + if err := x509Source.Close(); err != nil { + slog.Warn("Failed to close x509 source", "error", err) + } + }() + + svidUpdate := x509Source.Updated() + svid, err := x509Source.GetX509SVID() + if err != nil { + return fmt.Errorf("fetching initial X509 SVID: %w", err) + } + slog.Debug("Fetched initial X509 SVID", slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + "expires_at", svid.Certificates[0].NotAfter, + )) + + for { + slog.Debug("Exchanging X509 SVID for AWS credentials") + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + slog.Info( + "Successfully exchanged X509 SVID for AWS credentials", + ) + + expiresAt, err := time.Parse(time.RFC3339, credentials.Expiration) + if err != nil { + return fmt.Errorf("parsing expiration time: %w", err) + } + + slog.Debug("Writing AWS credentials to file", "path", awsCredentialsPath) + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credentials to file", "path", awsCredentialsPath) + + slog.Info( + "Sleeping until a new X509 SVID is received or the AWS credentials are close to expiry", + "aws_expires_at", expiresAt, + "aws_ttl", expiresAt.Sub(time.Now()), + "svid_expires_at", svid.Certificates[0].NotAfter, + "svid_ttl", svid.Certificates[0].NotAfter.Sub(time.Now()), + ) + select { + case <-time.After(time.Second * 10): + slog.Info("Triggering renewal as AWS credentials are close to expiry") + // TODO: Add case for AWS credential approaching expiry + case <-svidUpdate: + slog.Debug("Received potential X509 SVID update") + newSVID, err := x509Source.GetX509SVID() + if err != nil { + return fmt.Errorf("fetching updated X509 SVID: %w", err) + } + slog.Info( + "Received new X509 SVID from Workload API, will update AWS credentials", + slog.Group("svid", + "spiffe_id", newSVID.ID, + "hint", newSVID.Hint, + "expires_at", newSVID.Certificates[0].NotAfter, + ), + ) + svid = newSVID + case <-ctx.Done(): + return nil + } + } +} diff --git a/cmd/credential_process.go b/cmd/credential_process.go new file mode 100644 index 0000000..ae2d643 --- /dev/null +++ b/cmd/credential_process.go @@ -0,0 +1,70 @@ +package main + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + + "github.com/spf13/cobra" + "github.com/spiffe/go-spiffe/v2/workloadapi" +) + +func newX509CredentialProcessCmd() (*cobra.Command, error) { + sf := &sharedFlags{} + cmd := &cobra.Command{ + Use: "x509-credential-process", + Short: `Exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Compatible with the AWS credential process functionality.`, + Long: `Exchanges an X509 SVID for a short-lived set of AWS credentials using the AWS Roles Anywhere API. It returns the credentials to STDOUT, in the format expected by AWS SDKs and CLIs when invoking an external credential process.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + x509Ctx, err := client.FetchX509Context(ctx) + if err != nil { + return fmt.Errorf("fetching x509 context: %w", err) + } + // TODO(strideynet): Implement SVID selection mechanism, for now, + // we'll just use the first returned SVID (a.k.a the default). + svid := x509Ctx.DefaultSVID() + slog.Debug( + "Fetched X509 SVID", + slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + ), + ) + + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + + out, err := json.Marshal(credentials) + if err != nil { + return fmt.Errorf("marshalling credentials: %w", err) + } + _, err = os.Stdout.Write(out) + if err != nil { + return fmt.Errorf("writing credentials to stdout: %w", err) + } + return nil + }, + } + if err := sf.addFlags(cmd); err != nil { + return nil, fmt.Errorf("adding shared flags: %w", err) + } + + return cmd, nil +} diff --git a/cmd/main.go b/cmd/main.go index c487979..b58b9ca 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,19 +1,14 @@ package main import ( - "context" - "encoding/json" "fmt" "log/slog" "os" - "time" "github.com/spf13/cobra" awsspiffe "github.com/spiffe/aws-spiffe-workload-helper" - "github.com/spiffe/aws-spiffe-workload-helper/internal" "github.com/spiffe/aws-spiffe-workload-helper/internal/vendoredaws" "github.com/spiffe/go-spiffe/v2/svid/x509svid" - "github.com/spiffe/go-spiffe/v2/workloadapi" ) var ( @@ -54,11 +49,17 @@ func newRootCmd() (*cobra.Command, error) { } rootCmd.AddCommand(x509CredentialProcessCmd) - oneShotCredentialWriteCmd, err := newX509CredentialFileCmd() + x509CredentialFileCmd, err := newX509CredentialFileCmd() if err != nil { return nil, fmt.Errorf("initializing x509-credential-file command: %w", err) } - rootCmd.AddCommand(oneShotCredentialWriteCmd) + rootCmd.AddCommand(x509CredentialFileCmd) + + x509CredentialFileOneshotCmd, err := newX509CredentialFileOneshotCmd() + if err != nil { + return nil, fmt.Errorf("initializing x509-credential-file-oneshot command: %w", err) + } + rootCmd.AddCommand(x509CredentialFileOneshotCmd) return rootCmd, nil } @@ -121,269 +122,3 @@ func exchangeX509SVIDForAWSCredentials( ) return credentials, nil } - -func oneshotX509CredentialFile( - ctx context.Context, - force bool, - replace bool, - awsCredentialsPath string, - sf *sharedFlags, -) error { - client, err := workloadapi.New( - ctx, - workloadapi.WithAddr(sf.workloadAPIAddr), - ) - if err != nil { - return fmt.Errorf("creating workload api client: %w", err) - } - defer func() { - if err := client.Close(); err != nil { - slog.Warn("Failed to close workload API client", "error", err) - } - }() - - x509Ctx, err := client.FetchX509Context(ctx) - if err != nil { - return fmt.Errorf("fetching x509 context: %w", err) - } - svid := x509Ctx.DefaultSVID() - slog.Debug( - "Fetched X509 SVID", - slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - ), - ) - - credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) - if err != nil { - return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) - } - - // Now we write this to disk in the format that the AWS CLI/SDK - // expects for a credentials file. - err = internal.UpsertAWSCredentialsFileProfile( - slog.Default(), - internal.AWSCredentialsFileConfig{ - Path: awsCredentialsPath, - Force: force, - ReplaceFile: replace, - }, - internal.AWSCredentialsFileProfile{ - AWSAccessKeyID: credentials.AccessKeyId, - AWSSecretAccessKey: credentials.SecretAccessKey, - AWSSessionToken: credentials.SessionToken, - }, - ) - if err != nil { - return fmt.Errorf("writing credentials to file: %w", err) - } - slog.Info("Wrote AWS credential to file", "path", "./my-credential") - return nil -} - -func daemonX509CredentialFile( - ctx context.Context, - force bool, - replace bool, - awsCredentialsPath string, - sf *sharedFlags, -) error { - slog.Info("Starting AWS credential file daemon") - client, err := workloadapi.New( - ctx, - workloadapi.WithAddr(sf.workloadAPIAddr), - ) - if err != nil { - return fmt.Errorf("creating workload api client: %w", err) - } - defer func() { - if err := client.Close(); err != nil { - slog.Warn("Failed to close workload API client", "error", err) - } - }() - - slog.Debug("Fetching initial X509 SVID") - x509Source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClient(client)) - if err != nil { - return fmt.Errorf("creating x509 source: %w", err) - } - defer func() { - if err := x509Source.Close(); err != nil { - slog.Warn("Failed to close x509 source", "error", err) - } - }() - - svidUpdate := x509Source.Updated() - svid, err := x509Source.GetX509SVID() - if err != nil { - return fmt.Errorf("fetching initial X509 SVID: %w", err) - } - slog.Debug("Fetched initial X509 SVID", slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - "expires_at", svid.Certificates[0].NotAfter, - )) - - for { - slog.Debug("Exchanging X509 SVID for AWS credentials") - credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) - if err != nil { - return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) - } - slog.Info( - "Successfully exchanged X509 SVID for AWS credentials", - ) - - expiresAt, err := time.Parse(time.RFC3339, credentials.Expiration) - if err != nil { - return fmt.Errorf("parsing expiration time: %w", err) - } - - slog.Debug("Writing AWS credentials to file", "path", awsCredentialsPath) - err = internal.UpsertAWSCredentialsFileProfile( - slog.Default(), - internal.AWSCredentialsFileConfig{ - Path: awsCredentialsPath, - Force: force, - ReplaceFile: replace, - }, - internal.AWSCredentialsFileProfile{ - AWSAccessKeyID: credentials.AccessKeyId, - AWSSecretAccessKey: credentials.SecretAccessKey, - AWSSessionToken: credentials.SessionToken, - }, - ) - if err != nil { - return fmt.Errorf("writing credentials to file: %w", err) - } - slog.Info("Wrote AWS credentials to file", "path", awsCredentialsPath) - - slog.Info( - "Sleeping until a new X509 SVID is received or the AWS credentials are close to expiry", - "aws_expires_at", expiresAt, - "aws_ttl", expiresAt.Sub(time.Now()), - "svid_expires_at", svid.Certificates[0].NotAfter, - "svid_ttl", svid.Certificates[0].NotAfter.Sub(time.Now()), - ) - select { - case <-time.After(time.Second * 10): - slog.Info("Triggering renewal as AWS credentials are close to expiry") - // TODO: Add case for AWS credential approaching expiry - case <-svidUpdate: - slog.Debug("Received potential X509 SVID update") - newSVID, err := x509Source.GetX509SVID() - if err != nil { - return fmt.Errorf("fetching updated X509 SVID: %w", err) - } - slog.Info( - "Received new X509 SVID from Workload API, will update AWS credentials", - slog.Group("svid", - "spiffe_id", newSVID.ID, - "hint", newSVID.Hint, - "expires_at", newSVID.Certificates[0].NotAfter, - ), - ) - svid = newSVID - case <-ctx.Done(): - return nil - } - } - -} - -func newX509CredentialFileCmd() (*cobra.Command, error) { - oneshot := false - force := false - replace := false - awsCredentialsPath := "" - sf := &sharedFlags{} - cmd := &cobra.Command{ - Use: "x509-credential-file", - Short: ``, - Long: ``, - RunE: func(cmd *cobra.Command, args []string) error { - if oneshot { - return oneshotX509CredentialFile( - cmd.Context(), force, replace, awsCredentialsPath, sf, - ) - } - return daemonX509CredentialFile( - cmd.Context(), force, replace, awsCredentialsPath, sf, - ) - }, - // Hidden for now as the daemon is likely more "usable" - Hidden: true, - } - if err := sf.addFlags(cmd); err != nil { - return nil, fmt.Errorf("adding shared flags: %w", err) - } - cmd.Flags().BoolVar(&oneshot, "oneshot", false, "If set, generate the AWS credentials file and exit. If unset, run as a daemon that continuously updates the file before expiry or when a new SVID is available.") - cmd.Flags().StringVar(&awsCredentialsPath, "aws-credentials-path", "", "The path to the AWS credentials file to write.") - if err := cmd.MarkFlagRequired("aws-credentials-path"); err != nil { - return nil, fmt.Errorf("marking aws-credentials-path flag as required: %w", err) - } - cmd.Flags().BoolVar(&force, "force", false, "If set, failures loading the existing AWS credentials file will be ignored and the contents overwritten.") - cmd.Flags().BoolVar(&replace, "replace", false, "If set, the AWS credentials file will be replaced if it exists. This will remove any profiles not written by this tool.") - - return cmd, nil -} - -func newX509CredentialProcessCmd() (*cobra.Command, error) { - sf := &sharedFlags{} - cmd := &cobra.Command{ - Use: "x509-credential-process", - Short: `Exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Compatible with the AWS credential process functionality.`, - Long: `Exchanges an X509 SVID for a short-lived set of AWS credentials using the AWS Roles Anywhere API. It returns the credentials to STDOUT, in the format expected by AWS SDKs and CLIs when invoking an external credential process.`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := workloadapi.New( - ctx, - workloadapi.WithAddr(sf.workloadAPIAddr), - ) - if err != nil { - return fmt.Errorf("creating workload api client: %w", err) - } - defer func() { - if err := client.Close(); err != nil { - slog.Warn("Failed to close workload API client", "error", err) - } - }() - - x509Ctx, err := client.FetchX509Context(ctx) - if err != nil { - return fmt.Errorf("fetching x509 context: %w", err) - } - // TODO(strideynet): Implement SVID selection mechanism, for now, - // we'll just use the first returned SVID (a.k.a the default). - svid := x509Ctx.DefaultSVID() - slog.Debug( - "Fetched X509 SVID", - slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - ), - ) - - credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) - if err != nil { - return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) - } - - out, err := json.Marshal(credentials) - if err != nil { - return fmt.Errorf("marshalling credentials: %w", err) - } - _, err = os.Stdout.Write(out) - if err != nil { - return fmt.Errorf("writing credentials to stdout: %w", err) - } - return nil - }, - } - if err := sf.addFlags(cmd); err != nil { - return nil, fmt.Errorf("adding shared flags: %w", err) - } - - return cmd, nil -} From 10624f0e83992661e7bc1bb57c7c5c0bf00af067 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Mon, 2 Dec 2024 10:29:06 +0000 Subject: [PATCH 7/9] Create parent directories and renew on sane timer Signed-off-by: Noah Stride --- cmd/credential_file.go | 20 ++++++++++++++++---- internal/aws_credentials_file.go | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/cmd/credential_file.go b/cmd/credential_file.go index c25b42d..ec3e896 100644 --- a/cmd/credential_file.go +++ b/cmd/credential_file.go @@ -206,17 +206,29 @@ func daemonX509CredentialFile( } slog.Info("Wrote AWS credentials to file", "path", awsCredentialsPath) + // Calculate next renewal time as 50% of the remaining time left on the + // AWS credentials. + // TODO(noah): This is a little crude, it may make more sense to just + // renew on a fixed basis (e.g every minute?). We'll go with this + // for now, and speak to consumers once it's in use to see if a + // different mechanism may be more suitable. + now := time.Now() + awsTTL := expiresAt.Sub(now) + renewIn := awsTTL / 2 + awsRenewAt := now.Add(renewIn) + slog.Info( "Sleeping until a new X509 SVID is received or the AWS credentials are close to expiry", "aws_expires_at", expiresAt, - "aws_ttl", expiresAt.Sub(time.Now()), + "aws_ttl", awsTTL, + "aws_renews_at", awsRenewAt, "svid_expires_at", svid.Certificates[0].NotAfter, - "svid_ttl", svid.Certificates[0].NotAfter.Sub(time.Now()), + "svid_ttl", svid.Certificates[0].NotAfter.Sub(now), ) + select { - case <-time.After(time.Second * 10): + case <-time.After(time.Until(awsRenewAt)): slog.Info("Triggering renewal as AWS credentials are close to expiry") - // TODO: Add case for AWS credential approaching expiry case <-svidUpdate: slog.Debug("Received potential X509 SVID update") newSVID, err := x509Source.GetX509SVID() diff --git a/internal/aws_credentials_file.go b/internal/aws_credentials_file.go index 7f1c1c1..68eb471 100644 --- a/internal/aws_credentials_file.go +++ b/internal/aws_credentials_file.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "os" + "path/filepath" "gopkg.in/ini.v1" ) @@ -21,6 +22,16 @@ type AWSCredentialsFileProfile struct { AWSSessionToken string } +func ensureDirectory(fpath string) error { + dpath := filepath.Dir(fpath) + if _, err := os.Stat(dpath); os.IsNotExist(err) { + if err := os.MkdirAll(dpath, 0700); err != nil { + return fmt.Errorf("creating directory (%s): %w", dpath, err) + } + } + return nil +} + func loadAWSCredentialsFile( log *slog.Logger, cfg AWSCredentialsFileConfig, @@ -29,13 +40,17 @@ func loadAWSCredentialsFile( return ini.Empty(), nil } + // Create parent directory for file, should it be needed. + if err := ensureDirectory(cfg.Path); err != nil { + return nil, fmt.Errorf("ensuring parent directory: %w", err) + } + f, err := ini.Load(cfg.Path) if err == nil { return f, nil } // If it doesn't exist, we can "create" it. - // TODO: Make directory/parent directories if necessary. if os.IsNotExist(err) { return ini.Empty(), nil } From ecd01c6ac4ae9f46d35398fe1f3a64b37c80a058 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Mon, 2 Dec 2024 10:56:52 +0000 Subject: [PATCH 8/9] Update the help commands and slogging Signed-off-by: Noah Stride --- cmd/credential_file.go | 46 +++++++++++++++++++-------------------- cmd/credential_process.go | 8 +------ cmd/main.go | 8 +++++++ 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/cmd/credential_file.go b/cmd/credential_file.go index ec3e896..6788822 100644 --- a/cmd/credential_file.go +++ b/cmd/credential_file.go @@ -18,8 +18,8 @@ func newX509CredentialFileOneshotCmd() (*cobra.Command, error) { sf := &sharedFlags{} cmd := &cobra.Command{ Use: "x509-credential-file-oneshot", - Short: ``, - Long: ``, + Short: `Exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Writes the credentials to a file in the 'credential file' format expected by the AWS CLI and SDKs.`, + Long: `Exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Writes the credentials to a file in the 'credential file' format expected by the AWS CLI and SDKs.`, RunE: func(cmd *cobra.Command, args []string) error { return oneshotX509CredentialFile( cmd.Context(), force, replace, awsCredentialsPath, sf, @@ -64,12 +64,9 @@ func oneshotX509CredentialFile( return fmt.Errorf("fetching x509 context: %w", err) } svid := x509Ctx.DefaultSVID() - slog.Debug( + slog.Info( "Fetched X509 SVID", - slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - ), + "svid", svidValue(svid), ) credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) @@ -77,6 +74,11 @@ func oneshotX509CredentialFile( return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) } + expiresAt, err := time.Parse(time.RFC3339, credentials.Expiration) + if err != nil { + return fmt.Errorf("parsing expiration time: %w", err) + } + // Now we write this to disk in the format that the AWS CLI/SDK // expects for a credentials file. err = internal.UpsertAWSCredentialsFileProfile( @@ -95,7 +97,11 @@ func oneshotX509CredentialFile( if err != nil { return fmt.Errorf("writing credentials to file: %w", err) } - slog.Info("Wrote AWS credential to file", "path", "./my-credential") + slog.Info( + "Wrote AWS credential to file", + "path", awsCredentialsPath, + "aws_expires_at", expiresAt, + ) return nil } @@ -106,15 +112,13 @@ func newX509CredentialFileCmd() (*cobra.Command, error) { sf := &sharedFlags{} cmd := &cobra.Command{ Use: "x509-credential-file", - Short: ``, - Long: ``, + Short: `On a regular basis, this daemon exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Writes the credentials to a file in the 'credential file' format expected by the AWS CLI and SDKs.`, + Long: `On a regular basis, this daemon exchanges an X509 SVID for a short-lived set of AWS credentials using AWS Roles Anywhere. Writes the credentials to a file in the 'credential file' format expected by the AWS CLI and SDKs.`, RunE: func(cmd *cobra.Command, args []string) error { return daemonX509CredentialFile( cmd.Context(), force, replace, awsCredentialsPath, sf, ) }, - // Hidden for now as the daemon is likely more "usable" - Hidden: true, } if err := sf.addFlags(cmd); err != nil { return nil, fmt.Errorf("adding shared flags: %w", err) @@ -166,20 +170,20 @@ func daemonX509CredentialFile( if err != nil { return fmt.Errorf("fetching initial X509 SVID: %w", err) } - slog.Debug("Fetched initial X509 SVID", slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - "expires_at", svid.Certificates[0].NotAfter, - )) + slog.Info("Fetched initial X509 SVID", "svid", svidValue(svid)) for { - slog.Debug("Exchanging X509 SVID for AWS credentials") + slog.Debug( + "Exchanging X509 SVID for AWS credentials", + "svid", svidValue(svid), + ) credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) if err != nil { return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) } slog.Info( "Successfully exchanged X509 SVID for AWS credentials", + "svid", svidValue(svid), ) expiresAt, err := time.Parse(time.RFC3339, credentials.Expiration) @@ -237,11 +241,7 @@ func daemonX509CredentialFile( } slog.Info( "Received new X509 SVID from Workload API, will update AWS credentials", - slog.Group("svid", - "spiffe_id", newSVID.ID, - "hint", newSVID.Hint, - "expires_at", newSVID.Certificates[0].NotAfter, - ), + "svid", svidValue(svid), ) svid = newSVID case <-ctx.Done(): diff --git a/cmd/credential_process.go b/cmd/credential_process.go index ae2d643..79e762d 100644 --- a/cmd/credential_process.go +++ b/cmd/credential_process.go @@ -38,13 +38,7 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { // TODO(strideynet): Implement SVID selection mechanism, for now, // we'll just use the first returned SVID (a.k.a the default). svid := x509Ctx.DefaultSVID() - slog.Debug( - "Fetched X509 SVID", - slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - ), - ) + slog.Debug("Fetched X509 SVID", "svid", svidValue(svid)) credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) if err != nil { diff --git a/cmd/main.go b/cmd/main.go index b58b9ca..1e4fb83 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -122,3 +122,11 @@ func exchangeX509SVIDForAWSCredentials( ) return credentials, nil } + +func svidValue(svid *x509svid.SVID) slog.Value { + return slog.GroupValue( + slog.String("id", svid.ID.String()), + slog.String("hint", svid.Hint), + slog.Time("expires_at", svid.Certificates[0].NotAfter), + ) +} From 7b02989190b553168595aa225e2618a843f22144 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Mon, 2 Dec 2024 11:04:33 +0000 Subject: [PATCH 9/9] Update readme Signed-off-by: Noah Stride --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c54692e..4ac1bd2 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,50 @@ $ aws-spiffe-workload-helper x509-credential-process \ | session-duration | No | The duration, in seconds, of the resulting session. Optional. Can range from 15 minutes (900) to 12 hours (43200). | `3600` | | workload-api-addr | No | Overrides the address of the Workload API endpoint that will be use to fetch the X509 SVID. If unspecified, the value from the SPIFFE_ENDPOINT_SOCKET environment variable will be used. | `unix:///opt/my/path/workload.sock` | +#### `x509-credential-file` -### Configuring AWS SDKs and CLIs +The `x509-credential-file` command starts a long-lived daemon which exchanges +an X509 SVID for a short-lived set of AWS credentials using the AWS Roles +Anywhere API. It writes the credentials to a specified file in the format +supported by AWS SDKs and CLIs as a "credential file". + +It repeats this exchange process when the AWS credentials are more than 50% of +the way through their lifetime, ensuring that a fresh set of credentials are +always available. + +Whilst the `x509-credentials-process` flow should be preferred as it does not +cause credentials to be written to the filesystem, the `x509-credentials-file` +flow may be useful in scenarios where you need to provide credentials to legacy +SDKs or CLIs that do not support the `credential_process` configuration. + +The command fetches the X509-SVID from the SPIFFE Workload API. The location of +the SPIFFE Workload API endpoint should be specified using the +`SPIFFE_ENDPOINT_SOCKET` environment variable or the `--workload-api-addr` flag. + +```sh +$ aws-spiffe-workload-helper x509-credential-file \ + --trust-anchor-arn arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000 \ + --profile-arn arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-000000000000 \ + --role-arn arn:aws:iam::123456789012:role/example-role \ + --workload-api-addr unix:///opt/workload-api.sock \ + --aws-credentials-file /opt/my-aws-credentials-file +``` + +###### Reference + +| Flag | Required | Description | Example | +|----------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| role-arn | Yes | The ARN of the role to assume. Required. | `arn:aws:iam::123456789012:role/example-role` | +| profile-arn | Yes | The ARN of the Roles Anywhere profile to use. Required. | `arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000` | +| trust-anchor-arn | Yes | The ARN of the Roles Anywhere trust anchor to use. Required. | `arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000` | +| region | No | Overrides AWS region to use when exchanging the SVID for AWS credentials. Optional. | `us-east-1` | +| session-duration | No | The duration, in seconds, of the resulting session. Optional. Can range from 15 minutes (900) to 12 hours (43200). | `3600` | +| workload-api-addr | No | Overrides the address of the Workload API endpoint that will be use to fetch the X509 SVID. If unspecified, the value from the SPIFFE_ENDPOINT_SOCKET environment variable will be used. | `unix:///opt/my/path/workload.sock` | +| aws-credentials-path | Yes | The path to the AWS credentials file to write. | `/opt/my-aws-credentials-file | +| force | No | If set, failures loading the existing AWS credentials file will be ignored and the contents overwritten. | | +| replace | No | If set, the AWS credentials file will be replaced if it exists. This will remove any profiles not written by this tool. | | + +## Configuring AWS SDKs and CLIs To configure AWS SDKs and CLIs to use Roles Anywhere and SPIFFE for authentication, you will modify the AWS configuration file.