aboutsummaryrefslogtreecommitdiff
path: root/cloud
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-12-24 08:30:27 -0800
committerMike Crute <mike@crute.us>2022-12-24 08:30:27 -0800
commit4aac2b6026d27d4eba660b674bdb1f34bcfbfc54 (patch)
tree62eb040afb6d7f56f9bfb0af69cfc17ec2060a8b /cloud
parented1504c2826f6a5d406dd72e51f5a90b77ffea45 (diff)
downloadcloud-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.go137
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 @@
1package aws 1package aws
2 2
3import ( 3import (
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.
30type AWSClient interface { 35type 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.
38type account struct {
39 AccessKeyId string
40 SecretAccessKey string
41 Roles map[string]struct {
42 ARN string
43 ExternalId string
44 }
45}
46
47type client struct { 41type 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.
65func NewAWSClientFromAccount(a *models.Account) (AWSClient, error) { 57func 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.
90func 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.
133func (a *client) AssumeRole(user string, region *string) (*sts.Credentials, error) { 85func (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 }