Skip to content

Commit 61ac039

Browse files
authored
Merge pull request #684 from 1Password/aditya/aws-plugin-mfa-fix
Fix AWS MFA for roles assumed through a source profile
2 parents 577cd15 + db6da3e commit 61ac039

2 files changed

Lines changed: 126 additions & 11 deletions

File tree

‎plugins/aws/sts_provisioner.go‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ func (p STSProvisioner) Description() string {
9595

9696
// GetTemporaryCredentialsProviderForProfile returns the aws provider that fits the scenario described by the current configuration.
9797
func GetTemporaryCredentialsProviderForProfile(awsConfig *confighelpers.Config, providerFactory STSProviderFactory, itemFields map[sdk.FieldName]string) (aws.CredentialsProvider, error) {
98+
return getProviderForProfile(awsConfig, providerFactory, itemFields, nil)
99+
}
100+
101+
// getProviderForProfile applies MFA once, on the long-lived keys at the chain root: AWS rejects GetSessionToken
102+
// signed with session credentials. mfaConfig is the profile nearest the root seen so far with MFA configured.
103+
func getProviderForProfile(awsConfig *confighelpers.Config, providerFactory STSProviderFactory, itemFields map[sdk.FieldName]string, mfaConfig *confighelpers.Config) (aws.CredentialsProvider, error) {
98104
err := resolveLocalAnd1PasswordConfigurations(itemFields, awsConfig)
99105
if err != nil {
100106
return nil, err
@@ -113,25 +119,27 @@ func GetTemporaryCredentialsProviderForProfile(awsConfig *confighelpers.Config,
113119
return nil, fmt.Errorf(unsupportedMessage, "Credential Process Authentication", "https://github.com/1Password/shell-plugins/issues/213")
114120
}
115121

122+
if awsConfig.HasMfaSerial() && awsConfig.MfaToken != "" {
123+
mfaConfig = awsConfig
124+
}
125+
116126
var sourceCredentialsProvider aws.CredentialsProvider
117127
if awsConfig.HasSourceProfile() {
118-
sourceProfileProvider, err := GetTemporaryCredentialsProviderForProfile(awsConfig.SourceProfile, providerFactory, itemFields)
128+
sourceCredentialsProvider, err = getProviderForProfile(awsConfig.SourceProfile, providerFactory, itemFields, mfaConfig)
119129
if err != nil {
120130
return nil, err
121131
}
122-
sourceCredentialsProvider = sourceProfileProvider
123132
} else {
124133
sourceCredentialsProvider = providerFactory.NewAccessKeysProvider()
134+
if mfaConfig != nil {
135+
sourceCredentialsProvider = providerFactory.NewMFASessionTokenProvider(mfaConfig, sourceCredentialsProvider)
136+
}
125137
}
126138

127139
if awsConfig.HasRole() {
128140
return providerFactory.NewAssumeRoleProvider(awsConfig, sourceCredentialsProvider), nil
129141
}
130142

131-
if awsConfig.HasMfaSerial() && awsConfig.MfaToken != "" {
132-
return providerFactory.NewMFASessionTokenProvider(awsConfig, sourceCredentialsProvider), nil
133-
}
134-
135143
return sourceCredentialsProvider, nil
136144
}
137145

@@ -155,11 +163,6 @@ func (m CacheProviderFactory) NewAssumeRoleProvider(awsConfig *confighelpers.Con
155163
}
156164

157165
cacheWriter := NewSTSCacheWriter(roleCacheKey, m.OutCache)
158-
159-
if awsConfig.HasMfaSerial() && awsConfig.MfaToken != "" {
160-
return initAssumeRoleProvider(awsConfig, getSTSClient(awsConfig.Region, m.NewMFASessionTokenProvider(awsConfig, sourcedCredentialsProvider)), cacheWriter)
161-
}
162-
163166
return initAssumeRoleProvider(awsConfig, getSTSClient(awsConfig.Region, sourcedCredentialsProvider), cacheWriter)
164167
}
165168

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package aws
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/1Password/shell-plugins/sdk"
10+
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
11+
"github.com/aws/aws-sdk-go-v2/aws"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// The first two profiles are the ~/.aws/config from issue #559.
17+
const providerChainConfig = `[profile 1pass-source-profile]
18+
region = eu-west-1
19+
20+
[profile my-1password-profile]
21+
source_profile = 1pass-source-profile
22+
role_arn = arn:aws:iam::222222222222:role/MyRole
23+
region = eu-west-1
24+
25+
[profile direct-role]
26+
role_arn = arn:aws:iam::222222222222:role/MyRole
27+
region = eu-west-1
28+
29+
[profile chained-role]
30+
source_profile = my-1password-profile
31+
role_arn = arn:aws:iam::333333333333:role/ChainedRole
32+
region = eu-west-1
33+
34+
[profile role-with-local-mfa]
35+
source_profile = 1pass-source-profile
36+
role_arn = arn:aws:iam::222222222222:role/MyRole
37+
mfa_serial = arn:aws:iam::111111111111:mfa/some.user
38+
region = eu-west-1
39+
`
40+
41+
// stsCallChain lists the STS calls a provider makes, outermost first, ending
42+
// with what signs the innermost call.
43+
func stsCallChain(p aws.CredentialsProvider) []string {
44+
var chain []string
45+
for p != nil {
46+
switch v := p.(type) {
47+
case *assumeRoleProvider:
48+
chain = append(chain, "AssumeRole")
49+
p = v.StsClient.Options().Credentials
50+
case *mfaSessionTokenProvider:
51+
hop := "GetSessionToken"
52+
if v.Mfa != nil && v.GetMfaSerial() != "" {
53+
hop += "(MFA)"
54+
}
55+
chain = append(chain, hop)
56+
p = v.StsClient.Options().Credentials
57+
case accessKeysProvider:
58+
chain = append(chain, "long-lived-keys")
59+
p = nil
60+
default:
61+
chain = append(chain, fmt.Sprintf("%T", p))
62+
p = nil
63+
}
64+
}
65+
return chain
66+
}
67+
68+
func TestSTSProviderChain(t *testing.T) {
69+
configPath := filepath.Join(t.TempDir(), "awsConfig")
70+
require.NoError(t, os.WriteFile(configPath, []byte(providerChainConfig), 0600))
71+
t.Setenv("AWS_CONFIG_FILE", configPath)
72+
t.Setenv("AWS_PROFILE", "")
73+
t.Setenv("AWS_DEFAULT_REGION", "")
74+
75+
keysAndOTP := map[sdk.FieldName]string{
76+
fieldname.AccessKeyID: "AKIAHPIZFMD5EEXAMPLE",
77+
fieldname.SecretAccessKey: "lBfKB7P5ScmpxDeRoFLZvhJbqNGPoV0vIEXAMPLE",
78+
fieldname.OneTimePassword: "123456",
79+
}
80+
withItemSerial := map[sdk.FieldName]string{fieldname.MFASerial: "arn:aws:iam::111111111111:mfa/some.user"}
81+
for k, v := range keysAndOTP {
82+
withItemSerial[k] = v
83+
}
84+
85+
for _, c := range []struct {
86+
name, profile string
87+
itemFields map[sdk.FieldName]string
88+
want []string
89+
}{
90+
{"source_profile with role", "my-1password-profile", withItemSerial, []string{"AssumeRole", "GetSessionToken(MFA)", "long-lived-keys"}},
91+
{"role without source_profile", "direct-role", withItemSerial, []string{"AssumeRole", "GetSessionToken(MFA)", "long-lived-keys"}},
92+
{"role chained through role", "chained-role", withItemSerial, []string{"AssumeRole", "AssumeRole", "GetSessionToken(MFA)", "long-lived-keys"}},
93+
// mfa_serial set only on the role profile must still reach the chain root.
94+
{"mfa_serial only on role profile", "role-with-local-mfa", keysAndOTP, []string{"AssumeRole", "GetSessionToken(MFA)", "long-lived-keys"}},
95+
} {
96+
t.Run(c.name, func(t *testing.T) {
97+
awsConfig, err := getAWSAuthConfigurationForProfile(c.profile)
98+
require.NoError(t, err)
99+
100+
// Cold cache: nothing to short-circuit the STS calls.
101+
factory := &CacheProviderFactory{
102+
InCache: sdk.CacheState{},
103+
OutCache: sdk.CacheOperations{Puts: map[string]sdk.CacheEntry{}},
104+
ItemFields: c.itemFields,
105+
}
106+
provider, err := GetTemporaryCredentialsProviderForProfile(awsConfig, factory, c.itemFields)
107+
require.NoError(t, err)
108+
109+
assert.Equal(t, c.want, stsCallChain(provider))
110+
})
111+
}
112+
}

0 commit comments

Comments
 (0)