1package awsauth 2 3import ( 4 "context" 5 6 "github.com/aws/aws-sdk-go/aws" 7 "github.com/hashicorp/vault/sdk/framework" 8 "github.com/hashicorp/vault/sdk/logical" 9) 10 11func (b *backend) pathConfigClient() *framework.Path { 12 return &framework.Path{ 13 Pattern: "config/client$", 14 Fields: map[string]*framework.FieldSchema{ 15 "access_key": { 16 Type: framework.TypeString, 17 Default: "", 18 Description: "AWS Access Key ID for the account used to make AWS API requests.", 19 }, 20 21 "secret_key": { 22 Type: framework.TypeString, 23 Default: "", 24 Description: "AWS Secret Access Key for the account used to make AWS API requests.", 25 }, 26 27 "endpoint": { 28 Type: framework.TypeString, 29 Default: "", 30 Description: "URL to override the default generated endpoint for making AWS EC2 API calls.", 31 }, 32 33 "iam_endpoint": { 34 Type: framework.TypeString, 35 Default: "", 36 Description: "URL to override the default generated endpoint for making AWS IAM API calls.", 37 }, 38 39 "sts_endpoint": { 40 Type: framework.TypeString, 41 Default: "", 42 Description: "URL to override the default generated endpoint for making AWS STS API calls.", 43 }, 44 45 "iam_server_id_header_value": { 46 Type: framework.TypeString, 47 Default: "", 48 Description: "Value to require in the X-Vault-AWS-IAM-Server-ID request header", 49 }, 50 "max_retries": { 51 Type: framework.TypeInt, 52 Default: aws.UseServiceDefaultRetries, 53 Description: "Maximum number of retries for recoverable exceptions of AWS APIs", 54 }, 55 }, 56 57 ExistenceCheck: b.pathConfigClientExistenceCheck, 58 59 Operations: map[logical.Operation]framework.OperationHandler{ 60 logical.CreateOperation: &framework.PathOperation{ 61 Callback: b.pathConfigClientCreateUpdate, 62 }, 63 logical.UpdateOperation: &framework.PathOperation{ 64 Callback: b.pathConfigClientCreateUpdate, 65 }, 66 logical.DeleteOperation: &framework.PathOperation{ 67 Callback: b.pathConfigClientDelete, 68 }, 69 logical.ReadOperation: &framework.PathOperation{ 70 Callback: b.pathConfigClientRead, 71 }, 72 }, 73 74 HelpSynopsis: pathConfigClientHelpSyn, 75 HelpDescription: pathConfigClientHelpDesc, 76 } 77} 78 79// Establishes dichotomy of request operation between CreateOperation and UpdateOperation. 80// Returning 'true' forces an UpdateOperation, CreateOperation otherwise. 81func (b *backend) pathConfigClientExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 82 entry, err := b.lockedClientConfigEntry(ctx, req.Storage) 83 if err != nil { 84 return false, err 85 } 86 return entry != nil, nil 87} 88 89// Fetch the client configuration required to access the AWS API, after acquiring an exclusive lock. 90func (b *backend) lockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) { 91 b.configMutex.RLock() 92 defer b.configMutex.RUnlock() 93 94 return b.nonLockedClientConfigEntry(ctx, s) 95} 96 97// Fetch the client configuration required to access the AWS API. 98func (b *backend) nonLockedClientConfigEntry(ctx context.Context, s logical.Storage) (*clientConfig, error) { 99 entry, err := s.Get(ctx, "config/client") 100 if err != nil { 101 return nil, err 102 } 103 if entry == nil { 104 return nil, nil 105 } 106 107 var result clientConfig 108 if err := entry.DecodeJSON(&result); err != nil { 109 return nil, err 110 } 111 return &result, nil 112} 113 114func (b *backend) pathConfigClientRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 115 clientConfig, err := b.lockedClientConfigEntry(ctx, req.Storage) 116 if err != nil { 117 return nil, err 118 } 119 120 if clientConfig == nil { 121 return nil, nil 122 } 123 124 return &logical.Response{ 125 Data: map[string]interface{}{ 126 "access_key": clientConfig.AccessKey, 127 "endpoint": clientConfig.Endpoint, 128 "iam_endpoint": clientConfig.IAMEndpoint, 129 "sts_endpoint": clientConfig.STSEndpoint, 130 "iam_server_id_header_value": clientConfig.IAMServerIdHeaderValue, 131 "max_retries": clientConfig.MaxRetries, 132 }, 133 }, nil 134} 135 136func (b *backend) pathConfigClientDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 137 b.configMutex.Lock() 138 defer b.configMutex.Unlock() 139 140 if err := req.Storage.Delete(ctx, "config/client"); err != nil { 141 return nil, err 142 } 143 144 // Remove all the cached EC2 client objects in the backend. 145 b.flushCachedEC2Clients() 146 147 // Remove all the cached EC2 client objects in the backend. 148 b.flushCachedIAMClients() 149 150 // unset the cached default AWS account ID 151 b.defaultAWSAccountID = "" 152 153 return nil, nil 154} 155 156// pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key' 157// that can be used to interact with AWS EC2 API. 158func (b *backend) pathConfigClientCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 159 b.configMutex.Lock() 160 defer b.configMutex.Unlock() 161 162 configEntry, err := b.nonLockedClientConfigEntry(ctx, req.Storage) 163 if err != nil { 164 return nil, err 165 } 166 if configEntry == nil { 167 configEntry = &clientConfig{} 168 } 169 170 // changedCreds is whether we need to flush the cached AWS clients and store in the backend 171 changedCreds := false 172 // changedOtherConfig is whether other config has changed that requires storing in the backend 173 // but does not require flushing the cached clients 174 changedOtherConfig := false 175 176 accessKeyStr, ok := data.GetOk("access_key") 177 if ok { 178 if configEntry.AccessKey != accessKeyStr.(string) { 179 changedCreds = true 180 configEntry.AccessKey = accessKeyStr.(string) 181 } 182 } else if req.Operation == logical.CreateOperation { 183 // Use the default 184 configEntry.AccessKey = data.Get("access_key").(string) 185 } 186 187 secretKeyStr, ok := data.GetOk("secret_key") 188 if ok { 189 if configEntry.SecretKey != secretKeyStr.(string) { 190 changedCreds = true 191 configEntry.SecretKey = secretKeyStr.(string) 192 } 193 } else if req.Operation == logical.CreateOperation { 194 configEntry.SecretKey = data.Get("secret_key").(string) 195 } 196 197 endpointStr, ok := data.GetOk("endpoint") 198 if ok { 199 if configEntry.Endpoint != endpointStr.(string) { 200 changedCreds = true 201 configEntry.Endpoint = endpointStr.(string) 202 } 203 } else if req.Operation == logical.CreateOperation { 204 configEntry.Endpoint = data.Get("endpoint").(string) 205 } 206 207 iamEndpointStr, ok := data.GetOk("iam_endpoint") 208 if ok { 209 if configEntry.IAMEndpoint != iamEndpointStr.(string) { 210 changedCreds = true 211 configEntry.IAMEndpoint = iamEndpointStr.(string) 212 } 213 } else if req.Operation == logical.CreateOperation { 214 configEntry.IAMEndpoint = data.Get("iam_endpoint").(string) 215 } 216 217 stsEndpointStr, ok := data.GetOk("sts_endpoint") 218 if ok { 219 if configEntry.STSEndpoint != stsEndpointStr.(string) { 220 // We don't directly cache STS clients as they are ever directly used. 221 // However, they are potentially indirectly used as credential providers 222 // for the EC2 and IAM clients, and thus we would be indirectly caching 223 // them there. So, if we change the STS endpoint, we should flush those 224 // cached clients. 225 changedCreds = true 226 configEntry.STSEndpoint = stsEndpointStr.(string) 227 } 228 } else if req.Operation == logical.CreateOperation { 229 configEntry.STSEndpoint = data.Get("sts_endpoint").(string) 230 } 231 232 headerValStr, ok := data.GetOk("iam_server_id_header_value") 233 if ok { 234 if configEntry.IAMServerIdHeaderValue != headerValStr.(string) { 235 // NOT setting changedCreds here, since this isn't really cached 236 configEntry.IAMServerIdHeaderValue = headerValStr.(string) 237 changedOtherConfig = true 238 } 239 } else if req.Operation == logical.CreateOperation { 240 configEntry.IAMServerIdHeaderValue = data.Get("iam_server_id_header_value").(string) 241 } 242 243 maxRetriesInt, ok := data.GetOk("max_retries") 244 if ok { 245 configEntry.MaxRetries = maxRetriesInt.(int) 246 changedOtherConfig = true 247 } else if req.Operation == logical.CreateOperation { 248 configEntry.MaxRetries = data.Get("max_retries").(int) 249 } 250 251 // Since this endpoint supports both create operation and update operation, 252 // the error checks for access_key and secret_key not being set are not present. 253 // This allows calling this endpoint multiple times to provide the values. 254 // Hence, the readers of this endpoint should do the validation on 255 // the validation of keys before using them. 256 entry, err := logical.StorageEntryJSON("config/client", configEntry) 257 if err != nil { 258 return nil, err 259 } 260 261 if changedCreds || changedOtherConfig || req.Operation == logical.CreateOperation { 262 if err := req.Storage.Put(ctx, entry); err != nil { 263 return nil, err 264 } 265 } 266 267 if changedCreds { 268 b.flushCachedEC2Clients() 269 b.flushCachedIAMClients() 270 b.defaultAWSAccountID = "" 271 } 272 273 return nil, nil 274} 275 276// Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to 277// interact with the AWS EC2 API. 278type clientConfig struct { 279 AccessKey string `json:"access_key"` 280 SecretKey string `json:"secret_key"` 281 Endpoint string `json:"endpoint"` 282 IAMEndpoint string `json:"iam_endpoint"` 283 STSEndpoint string `json:"sts_endpoint"` 284 IAMServerIdHeaderValue string `json:"iam_server_id_header_value"` 285 MaxRetries int `json:"max_retries"` 286} 287 288const pathConfigClientHelpSyn = ` 289Configure AWS IAM credentials that are used to query instance and role details from the AWS API. 290` 291 292const pathConfigClientHelpDesc = ` 293The aws-ec2 auth method makes AWS API queries to retrieve information 294regarding EC2 instances that perform login operations. The 'aws_secret_key' and 295'aws_access_key' parameters configured here should map to an AWS IAM user that 296has permission to make the following API queries: 297 298* ec2:DescribeInstances 299* iam:GetInstanceProfile (if IAM Role binding is used) 300` 301