diff options
author | Mike Crute <mike@crute.us> | 2022-12-24 08:30:27 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2022-12-24 08:30:27 -0800 |
commit | 4aac2b6026d27d4eba660b674bdb1f34bcfbfc54 (patch) | |
tree | 62eb040afb6d7f56f9bfb0af69cfc17ec2060a8b /cloud | |
parent | ed1504c2826f6a5d406dd72e51f5a90b77ffea45 (diff) | |
download | cloud-identity-broker-4aac2b6026d27d4eba660b674bdb1f34bcfbfc54.tar.bz2 cloud-identity-broker-4aac2b6026d27d4eba660b674bdb1f34bcfbfc54.tar.xz cloud-identity-broker-4aac2b6026d27d4eba660b674bdb1f34bcfbfc54.zip |
Use Vault for IAM users
Diffstat (limited to 'cloud')
-rw-r--r-- | cloud/aws/aws.go | 137 |
1 files changed, 50 insertions, 87 deletions
diff --git a/cloud/aws/aws.go b/cloud/aws/aws.go index 36ac338..3229dd2 100644 --- a/cloud/aws/aws.go +++ b/cloud/aws/aws.go | |||
@@ -1,6 +1,7 @@ | |||
1 | package aws | 1 | package aws |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "context" | ||
4 | "encoding/json" | 5 | "encoding/json" |
5 | "fmt" | 6 | "fmt" |
6 | "io/ioutil" | 7 | "io/ioutil" |
@@ -9,9 +10,10 @@ import ( | |||
9 | "strconv" | 10 | "strconv" |
10 | 11 | ||
11 | "code.crute.us/mcrute/cloud-identity-broker/app/models" | 12 | "code.crute.us/mcrute/cloud-identity-broker/app/models" |
13 | "code.crute.us/mcrute/golib/secrets" | ||
12 | 14 | ||
13 | "code.crute.us/mcrute/golib/vault" | ||
14 | "github.com/aws/aws-sdk-go/aws" | 15 | "github.com/aws/aws-sdk-go/aws" |
16 | "github.com/aws/aws-sdk-go/aws/awserr" | ||
15 | "github.com/aws/aws-sdk-go/aws/credentials" | 17 | "github.com/aws/aws-sdk-go/aws/credentials" |
16 | "github.com/aws/aws-sdk-go/aws/session" | 18 | "github.com/aws/aws-sdk-go/aws/session" |
17 | "github.com/aws/aws-sdk-go/service/ec2" | 19 | "github.com/aws/aws-sdk-go/service/ec2" |
@@ -27,28 +29,18 @@ type Region struct { | |||
27 | Enabled bool | 29 | Enabled bool |
28 | } | 30 | } |
29 | 31 | ||
32 | // AWSClient is a client for working with the AWS APIs. | ||
33 | // | ||
34 | // Instances of AWSClient are safe for concurrent access. | ||
30 | type AWSClient interface { | 35 | type AWSClient interface { |
31 | AssumeRole(string, *string) (*sts.Credentials, error) | 36 | AssumeRole(string, *string) (*sts.Credentials, error) |
32 | GetFederationURL(string, string) (string, error) | 37 | GetFederationURL(string, string) (string, error) |
33 | GetRegionList() ([]*Region, error) | 38 | GetRegionList() ([]*Region, error) |
34 | } | 39 | } |
35 | 40 | ||
36 | // account models the account configuration stored in Vault for an AWS account | ||
37 | // with assumable roles that are stored within a kv JSON record. | ||
38 | type account struct { | ||
39 | AccessKeyId string | ||
40 | SecretAccessKey string | ||
41 | Roles map[string]struct { | ||
42 | ARN string | ||
43 | ExternalId string | ||
44 | } | ||
45 | } | ||
46 | |||
47 | type client struct { | 41 | type client struct { |
48 | AccessKeyId string | 42 | Credentials credentials.Value |
49 | SecretAccessKey string | ||
50 | ARN string | 43 | ARN string |
51 | ExternalId string | ||
52 | ConsoleSessionDurationSecs int64 | 44 | ConsoleSessionDurationSecs int64 |
53 | } | 45 | } |
54 | 46 | ||
@@ -62,61 +54,22 @@ var _ AWSClient = (*client)(nil) | |||
62 | // which is used as the scope for this AWS client. Thus even if an account has | 54 | // which is used as the scope for this AWS client. Thus even if an account has |
63 | // multiple roles there must be one instance of the AWS client per account/role | 55 | // multiple roles there must be one instance of the AWS client per account/role |
64 | // pair. | 56 | // pair. |
65 | func NewAWSClientFromAccount(a *models.Account) (AWSClient, error) { | 57 | func NewAWSClientFromAccount(ctx context.Context, a *models.Account, sc secrets.Client) (AWSClient, error) { |
66 | var ac account | 58 | u, _, err := sc.AWSIAMUser(ctx, a.AdminVaultMaterial) |
67 | if err := vault.GetVaultKeyStruct(a.VaultMaterial, &ac); err != nil { | 59 | if err != nil { |
68 | return nil, err | 60 | return nil, err |
69 | } | 61 | } |
70 | 62 | ||
71 | r, ok := ac.Roles[a.ShortName] | ||
72 | if !ok { | ||
73 | return nil, fmt.Errorf("No roles for account %s in vault response", a.ShortName) | ||
74 | } | ||
75 | |||
76 | return &client{ | 63 | return &client{ |
77 | AccessKeyId: ac.AccessKeyId, | 64 | Credentials: credentials.Value{ |
78 | SecretAccessKey: ac.SecretAccessKey, | 65 | AccessKeyID: u.AccessKeyId, |
79 | ARN: r.ARN, | 66 | SecretAccessKey: u.SecretAccessKey, |
80 | ExternalId: r.ExternalId, | 67 | }, |
68 | ARN: a.AssumedRoleARN, | ||
81 | ConsoleSessionDurationSecs: a.ConsoleSessionDurationSecs(), | 69 | ConsoleSessionDurationSecs: a.ConsoleSessionDurationSecs(), |
82 | }, nil | 70 | }, nil |
83 | } | 71 | } |
84 | 72 | ||
85 | // ValidateVaultMaterial is used to check that a Vault material can be accessed | ||
86 | // and that the shape of that material is correct for an AWS access key and | ||
87 | // role list. | ||
88 | // | ||
89 | // This should be used for admission control for the creation of new accounts. | ||
90 | func ValidateVaultMaterial(m string) error { | ||
91 | var ac account | ||
92 | if err := vault.GetVaultKeyStruct(m, &ac); err != nil { | ||
93 | return fmt.Errorf("Unable to access vault material: %w", err) | ||
94 | } | ||
95 | |||
96 | if ac.AccessKeyId == "" { | ||
97 | return fmt.Errorf("AccessKeyId is empty") | ||
98 | } | ||
99 | |||
100 | if ac.SecretAccessKey == "" { | ||
101 | return fmt.Errorf("SecretAccessKey is empty") | ||
102 | } | ||
103 | |||
104 | if len(ac.Roles) == 0 { | ||
105 | return fmt.Errorf("No roles specified") | ||
106 | } | ||
107 | |||
108 | for k, r := range ac.Roles { | ||
109 | if r.ARN == "" { | ||
110 | return fmt.Errorf("ARN for role %s is empty", k) | ||
111 | } | ||
112 | if r.ExternalId == "" { | ||
113 | return fmt.Errorf("ExternalId for role %s is empty", k) | ||
114 | } | ||
115 | } | ||
116 | |||
117 | return nil | ||
118 | } | ||
119 | |||
120 | // AssumeRole uses an IAM user credential with higher privilege to assume a | 73 | // AssumeRole uses an IAM user credential with higher privilege to assume a |
121 | // role in an AWS account and region. It returns the STS credentials. | 74 | // role in an AWS account and region. It returns the STS credentials. |
122 | // | 75 | // |
@@ -124,32 +77,34 @@ func ValidateVaultMaterial(m string) error { | |||
124 | // regions AWS has been siloing assumed role credentials to that region so it's | 77 | // regions AWS has been siloing assumed role credentials to that region so it's |
125 | // important to use the correct regional endpoint to fetch the credentials. | 78 | // important to use the correct regional endpoint to fetch the credentials. |
126 | // | 79 | // |
127 | // Note that this is not simply a passthrough to Vault's AWS backend because | 80 | // Note that this is not simply a passthrough to Vault's AWS backend |
128 | // the Vault backend works by assuming roles and when assuming roles with an | 81 | // because the Vault backend will only call AssumeRole within the region |
129 | // assumed role AWS limits the chained role lifetime to 1 hour which doesn't | 82 | // of the root account configured in Vault and clients of this method |
130 | // work depending on how the upstream web application tied to this client | 83 | // need to be able to make the correct calls in the region of their |
131 | // works. This method instead uses a long-lived IAM user credential to assume a | 84 | // choice. |
132 | // role, which has a limited lifetime which is typically greater than 1 hour. | ||
133 | func (a *client) AssumeRole(user string, region *string) (*sts.Credentials, error) { | 85 | func (a *client) AssumeRole(user string, region *string) (*sts.Credentials, error) { |
134 | if region != nil && *region == "global" { | 86 | if region != nil && *region == "global" { |
135 | region = nil | 87 | region = nil |
136 | } | 88 | } |
137 | 89 | ||
138 | c := sts.New(session.New(&aws.Config{ | 90 | s, err := session.NewSessionWithOptions(session.Options{ |
139 | Region: region, | 91 | Config: aws.Config{ |
140 | Credentials: credentials.NewStaticCredentials( | 92 | Region: region, |
141 | a.AccessKeyId, | 93 | Credentials: credentials.NewStaticCredentialsFromCreds(a.Credentials), |
142 | a.SecretAccessKey, | 94 | }, |
143 | "", | 95 | SharedConfigState: session.SharedConfigDisable, |
144 | ), | 96 | }) |
145 | })) | 97 | if err != nil { |
146 | result, err := c.AssumeRole(&sts.AssumeRoleInput{ | 98 | return nil, err |
147 | ExternalId: aws.String(a.ExternalId), | 99 | } |
100 | |||
101 | result, err := sts.New(s).AssumeRole(&sts.AssumeRoleInput{ | ||
148 | RoleArn: aws.String(a.ARN), | 102 | RoleArn: aws.String(a.ARN), |
149 | RoleSessionName: aws.String(user), | 103 | RoleSessionName: aws.String(user), |
150 | DurationSeconds: aws.Int64(a.ConsoleSessionDurationSecs), | 104 | DurationSeconds: aws.Int64(a.ConsoleSessionDurationSecs), |
151 | }) | 105 | }) |
152 | if err != nil { | 106 | if err != nil { |
107 | fmt.Printf("AWS AssumeRole Error: %s\n", err.(awserr.Error).Message()) | ||
153 | return nil, err | 108 | return nil, err |
154 | } | 109 | } |
155 | 110 | ||
@@ -167,16 +122,24 @@ func (a *client) GetRegionList() ([]*Region, error) { | |||
167 | return nil, err | 122 | return nil, err |
168 | } | 123 | } |
169 | 124 | ||
170 | ec2c := ec2.New(session.New(&aws.Config{ | 125 | s, err := session.NewSessionWithOptions(session.Options{ |
171 | Region: defaultRegion, | 126 | Config: aws.Config{ |
172 | Credentials: credentials.NewStaticCredentials( | 127 | Region: defaultRegion, |
173 | *r.AccessKeyId, | 128 | Credentials: credentials.NewStaticCredentials( |
174 | *r.SecretAccessKey, | 129 | *r.AccessKeyId, |
175 | *r.SessionToken, | 130 | *r.SecretAccessKey, |
176 | ), | 131 | *r.SessionToken, |
177 | })) | 132 | ), |
133 | }, | ||
134 | SharedConfigState: session.SharedConfigDisable, | ||
135 | }) | ||
136 | if err != nil { | ||
137 | return nil, err | ||
138 | } | ||
178 | 139 | ||
179 | ro, err := ec2c.DescribeRegions(&ec2.DescribeRegionsInput{AllRegions: aws.Bool(true)}) | 140 | ro, err := ec2.New(s).DescribeRegions(&ec2.DescribeRegionsInput{ |
141 | AllRegions: aws.Bool(true), | ||
142 | }) | ||
180 | if err != nil { | 143 | if err != nil { |
181 | return nil, err | 144 | return nil, err |
182 | } | 145 | } |