1package awsauth 2 3import ( 4 "context" 5 "fmt" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws/endpoints" 11 "github.com/aws/aws-sdk-go/service/ec2" 12 "github.com/aws/aws-sdk-go/service/iam" 13 "github.com/hashicorp/vault/sdk/framework" 14 "github.com/hashicorp/vault/sdk/helper/awsutil" 15 "github.com/hashicorp/vault/sdk/helper/consts" 16 "github.com/hashicorp/vault/sdk/logical" 17 cache "github.com/patrickmn/go-cache" 18) 19 20const amzHeaderPrefix = "X-Amz-" 21 22var defaultAllowedSTSRequestHeaders = []string{ 23 "X-Amz-Algorithm", 24 "X-Amz-Content-Sha256", 25 "X-Amz-Credential", 26 "X-Amz-Date", 27 "X-Amz-Security-Token", 28 "X-Amz-Signature", 29 "X-Amz-SignedHeaders", 30} 31 32func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 33 b, err := Backend(conf) 34 if err != nil { 35 return nil, err 36 } 37 if err := b.Setup(ctx, conf); err != nil { 38 return nil, err 39 } 40 return b, nil 41} 42 43type backend struct { 44 *framework.Backend 45 46 // Lock to make changes to any of the backend's configuration endpoints. 47 configMutex sync.RWMutex 48 49 // Lock to make changes to role entries 50 roleMutex sync.Mutex 51 52 // Lock to make changes to the deny list entries 53 denyListMutex sync.RWMutex 54 55 // Guards the deny list/access list tidy functions 56 tidyDenyListCASGuard *uint32 57 tidyAccessListCASGuard *uint32 58 59 // Duration after which the periodic function of the backend needs to 60 // tidy the deny list and access list entries. 61 tidyCooldownPeriod time.Duration 62 63 // nextTidyTime holds the time at which the periodic func should initiate 64 // the tidy operations. This is set by the periodicFunc based on the value 65 // of tidyCooldownPeriod. 66 nextTidyTime time.Time 67 68 // Map to hold the EC2 client objects indexed by region and STS role. 69 // This avoids the overhead of creating a client object for every login request. 70 // When the credentials are modified or deleted, all the cached client objects 71 // will be flushed. The empty STS role signifies the master account 72 EC2ClientsMap map[string]map[string]*ec2.EC2 73 74 // Map to hold the IAM client objects indexed by region and STS role. 75 // This avoids the overhead of creating a client object for every login request. 76 // When the credentials are modified or deleted, all the cached client objects 77 // will be flushed. The empty STS role signifies the master account 78 IAMClientsMap map[string]map[string]*iam.IAM 79 80 // Map to associate a partition to a random region in that partition. Users of 81 // this don't care what region in the partition they use, but there is some client 82 // cache efficiency gain if we keep the mapping stable, hence caching a single copy. 83 partitionToRegionMap map[string]*endpoints.Region 84 85 // Map of AWS unique IDs to the full ARN corresponding to that unique ID 86 // This avoids the overhead of an AWS API hit for every login request 87 // using the IAM auth method when bound_iam_principal_arn contains a wildcard 88 iamUserIdToArnCache *cache.Cache 89 90 // AWS Account ID of the "default" AWS credentials 91 // This cache avoids the need to call GetCallerIdentity repeatedly to learn it 92 // We can't store this because, in certain pathological cases, it could change 93 // out from under us, such as a standby and active Vault server in different AWS 94 // accounts using their IAM instance profile to get their credentials. 95 defaultAWSAccountID string 96 97 // roleCache caches role entries to avoid locking headaches 98 roleCache *cache.Cache 99 100 resolveArnToUniqueIDFunc func(context.Context, logical.Storage, string) (string, error) 101 102 // upgradeCancelFunc is used to cancel the context used in the upgrade 103 // function 104 upgradeCancelFunc context.CancelFunc 105 106 // deprecatedTerms is used to downgrade preferred terminology (e.g. accesslist) 107 // to the legacy term. This allows for consolidated aliasing of the affected 108 // endpoints until the legacy terms are removed. 109 deprecatedTerms *strings.Replacer 110} 111 112func Backend(_ *logical.BackendConfig) (*backend, error) { 113 b := &backend{ 114 // Setting the periodic func to be run once in an hour. 115 // If there is a real need, this can be made configurable. 116 tidyCooldownPeriod: time.Hour, 117 EC2ClientsMap: make(map[string]map[string]*ec2.EC2), 118 IAMClientsMap: make(map[string]map[string]*iam.IAM), 119 iamUserIdToArnCache: cache.New(7*24*time.Hour, 24*time.Hour), 120 tidyDenyListCASGuard: new(uint32), 121 tidyAccessListCASGuard: new(uint32), 122 roleCache: cache.New(cache.NoExpiration, cache.NoExpiration), 123 124 deprecatedTerms: strings.NewReplacer( 125 "accesslist", "whitelist", 126 "denylist", "blacklist", 127 ), 128 } 129 130 b.resolveArnToUniqueIDFunc = b.resolveArnToRealUniqueId 131 132 b.Backend = &framework.Backend{ 133 PeriodicFunc: b.periodicFunc, 134 AuthRenew: b.pathLoginRenew, 135 Help: backendHelp, 136 PathsSpecial: &logical.Paths{ 137 Unauthenticated: []string{ 138 "login", 139 }, 140 LocalStorage: []string{ 141 identityAccessListStorage, 142 }, 143 SealWrapStorage: []string{ 144 "config/client", 145 }, 146 }, 147 Paths: []*framework.Path{ 148 b.pathLogin(), 149 b.pathListRole(), 150 b.pathListRoles(), 151 b.pathRole(), 152 b.pathRoleTag(), 153 b.pathConfigClient(), 154 b.pathConfigCertificate(), 155 b.pathConfigIdentity(), 156 b.pathConfigRotateRoot(), 157 b.pathConfigSts(), 158 b.pathListSts(), 159 b.pathListCertificates(), 160 161 // The following pairs of functions are path aliases. The first is the 162 // primary endpoint, and the second is version using deprecated language, 163 // for backwards compatibility. The functionality is identical between the two. 164 b.pathConfigTidyRoletagDenyList(), 165 b.genDeprecatedPath(b.pathConfigTidyRoletagDenyList()), 166 167 b.pathConfigTidyIdentityAccessList(), 168 b.genDeprecatedPath(b.pathConfigTidyIdentityAccessList()), 169 170 b.pathListRoletagDenyList(), 171 b.genDeprecatedPath(b.pathListRoletagDenyList()), 172 173 b.pathRoletagDenyList(), 174 b.genDeprecatedPath(b.pathRoletagDenyList()), 175 176 b.pathTidyRoletagDenyList(), 177 b.genDeprecatedPath(b.pathTidyRoletagDenyList()), 178 179 b.pathListIdentityAccessList(), 180 b.genDeprecatedPath(b.pathListIdentityAccessList()), 181 182 b.pathIdentityAccessList(), 183 b.genDeprecatedPath(b.pathIdentityAccessList()), 184 185 b.pathTidyIdentityAccessList(), 186 b.genDeprecatedPath(b.pathTidyIdentityAccessList()), 187 }, 188 Invalidate: b.invalidate, 189 InitializeFunc: b.initialize, 190 BackendType: logical.TypeCredential, 191 Clean: b.cleanup, 192 } 193 194 b.partitionToRegionMap = generatePartitionToRegionMap() 195 196 return b, nil 197} 198 199// periodicFunc performs the tasks that the backend wishes to do periodically. 200// Currently this will be triggered once in a minute by the RollbackManager. 201// 202// The tasks being done currently by this function are to cleanup the expired 203// entries of both deny list role tags and access list identities. Tidying is done 204// not once in a minute, but once in an hour, controlled by 'tidyCooldownPeriod'. 205// Tidying of deny list and access list are by default enabled. This can be 206// changed using `config/tidy/roletags` and `config/tidy/identities` endpoints. 207func (b *backend) periodicFunc(ctx context.Context, req *logical.Request) error { 208 // Run the tidy operations for the first time. Then run it when current 209 // time matches the nextTidyTime. 210 if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { 211 if b.System().LocalMount() || !b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary|consts.ReplicationPerformanceStandby) { 212 // safetyBuffer defaults to 180 days for roletag deny list 213 safetyBuffer := 15552000 214 tidyBlacklistConfigEntry, err := b.lockedConfigTidyRoleTags(ctx, req.Storage) 215 if err != nil { 216 return err 217 } 218 skipBlacklistTidy := false 219 // check if tidying of role tags was configured 220 if tidyBlacklistConfigEntry != nil { 221 // check if periodic tidying of role tags was disabled 222 if tidyBlacklistConfigEntry.DisablePeriodicTidy { 223 skipBlacklistTidy = true 224 } 225 // overwrite the default safetyBuffer with the configured value 226 safetyBuffer = tidyBlacklistConfigEntry.SafetyBuffer 227 } 228 // tidy role tags if explicitly not disabled 229 if !skipBlacklistTidy { 230 b.tidyDenyListRoleTag(ctx, req, safetyBuffer) 231 } 232 } 233 234 // We don't check for replication state for access list identities as 235 // these are locally stored 236 237 safety_buffer := 259200 238 tidyWhitelistConfigEntry, err := b.lockedConfigTidyIdentities(ctx, req.Storage) 239 if err != nil { 240 return err 241 } 242 skipWhitelistTidy := false 243 // check if tidying of identities was configured 244 if tidyWhitelistConfigEntry != nil { 245 // check if periodic tidying of identities was disabled 246 if tidyWhitelistConfigEntry.DisablePeriodicTidy { 247 skipWhitelistTidy = true 248 } 249 // overwrite the default safety_buffer with the configured value 250 safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer 251 } 252 // tidy identities if explicitly not disabled 253 if !skipWhitelistTidy { 254 b.tidyAccessListIdentity(ctx, req, safety_buffer) 255 } 256 257 // Update the time at which to run the tidy functions again. 258 b.nextTidyTime = time.Now().Add(b.tidyCooldownPeriod) 259 } 260 return nil 261} 262 263func (b *backend) cleanup(ctx context.Context) { 264 if b.upgradeCancelFunc != nil { 265 b.upgradeCancelFunc() 266 } 267} 268 269func (b *backend) invalidate(ctx context.Context, key string) { 270 switch { 271 case key == "config/client": 272 b.configMutex.Lock() 273 defer b.configMutex.Unlock() 274 b.flushCachedEC2Clients() 275 b.flushCachedIAMClients() 276 b.defaultAWSAccountID = "" 277 case strings.HasPrefix(key, "role"): 278 // TODO: We could make this better 279 b.roleCache.Flush() 280 } 281} 282 283// Putting this here so we can inject a fake resolver into the backend for unit testing 284// purposes 285func (b *backend) resolveArnToRealUniqueId(ctx context.Context, s logical.Storage, arn string) (string, error) { 286 entity, err := parseIamArn(arn) 287 if err != nil { 288 return "", err 289 } 290 // This odd-looking code is here because IAM is an inherently global service. IAM and STS ARNs 291 // don't have regions in them, and there is only a single global endpoint for IAM; see 292 // http://docs.aws.amazon.com/general/latest/gr/rande.html#iam_region 293 // However, the ARNs do have a partition in them, because the GovCloud and China partitions DO 294 // have their own separate endpoints, and the partition is encoded in the ARN. If Amazon's Go SDK 295 // would allow us to pass a partition back to the IAM client, it would be much simpler. But it 296 // doesn't appear that's possible, so in order to properly support GovCloud and China, we do a 297 // circular dance of extracting the partition from the ARN, finding any arbitrary region in the 298 // partition, and passing that region back back to the SDK, so that the SDK can figure out the 299 // proper partition from the arbitrary region we passed in to look up the endpoint. 300 // Sigh 301 region := b.partitionToRegionMap[entity.Partition] 302 if region == nil { 303 return "", fmt.Errorf("unable to resolve partition %q to a region", entity.Partition) 304 } 305 iamClient, err := b.clientIAM(ctx, s, region.ID(), entity.AccountNumber) 306 if err != nil { 307 return "", awsutil.AppendAWSError(err) 308 } 309 310 switch entity.Type { 311 case "user": 312 userInfo, err := iamClient.GetUser(&iam.GetUserInput{UserName: &entity.FriendlyName}) 313 if err != nil { 314 return "", awsutil.AppendAWSError(err) 315 } 316 if userInfo == nil { 317 return "", fmt.Errorf("got nil result from GetUser") 318 } 319 return *userInfo.User.UserId, nil 320 case "role": 321 roleInfo, err := iamClient.GetRole(&iam.GetRoleInput{RoleName: &entity.FriendlyName}) 322 if err != nil { 323 return "", awsutil.AppendAWSError(err) 324 } 325 if roleInfo == nil { 326 return "", fmt.Errorf("got nil result from GetRole") 327 } 328 return *roleInfo.Role.RoleId, nil 329 case "instance-profile": 330 profileInfo, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{InstanceProfileName: &entity.FriendlyName}) 331 if err != nil { 332 return "", awsutil.AppendAWSError(err) 333 } 334 if profileInfo == nil { 335 return "", fmt.Errorf("got nil result from GetInstanceProfile") 336 } 337 return *profileInfo.InstanceProfile.InstanceProfileId, nil 338 default: 339 return "", fmt.Errorf("unrecognized error type %#v", entity.Type) 340 } 341} 342 343// genDeprecatedPath will return a deprecated version of a framework.Path. The will include 344// using deprecated terms in the path pattern, and marking the path as deprecated. 345func (b *backend) genDeprecatedPath(path *framework.Path) *framework.Path { 346 pathDeprecated := *path 347 pathDeprecated.Pattern = b.deprecatedTerms.Replace(path.Pattern) 348 pathDeprecated.Deprecated = true 349 350 return &pathDeprecated 351} 352 353// Adapted from https://docs.aws.amazon.com/sdk-for-go/api/aws/endpoints/ 354// the "Enumerating Regions and Endpoint Metadata" section 355func generatePartitionToRegionMap() map[string]*endpoints.Region { 356 partitionToRegion := make(map[string]*endpoints.Region) 357 358 resolver := endpoints.DefaultResolver() 359 partitions := resolver.(endpoints.EnumPartitions).Partitions() 360 361 for _, p := range partitions { 362 // For most partitions, it's fine to choose a single region randomly. 363 // However, there are a few exceptions: 364 // 365 // For "aws", choose "us-east-1" because it is always enabled (and 366 // enabled for STS) by default. 367 // 368 // For "aws-us-gov", choose "us-gov-west-1" because it is the only 369 // valid region for IAM operations. 370 // ref: https://github.com/aws/aws-sdk-go/blob/v1.34.25/aws/endpoints/defaults.go#L8176-L8194 371 for _, r := range p.Regions() { 372 if p.ID() == "aws" && r.ID() != "us-east-1" { 373 continue 374 } 375 if p.ID() == "aws-us-gov" && r.ID() != "us-gov-west-1" { 376 continue 377 } 378 partitionToRegion[p.ID()] = &r 379 break 380 } 381 } 382 383 return partitionToRegion 384} 385 386const backendHelp = ` 387The aws auth method uses either AWS IAM credentials or AWS-signed EC2 metadata 388to authenticate clients, which are IAM principals or EC2 instances. 389 390Authentication is backed by a preconfigured role in the backend. The role 391represents the authorization of resources by containing Vault's policies. 392Role can be created using 'role/<role>' endpoint. 393 394Authentication of IAM principals, either IAM users or roles, is done using a 395specifically signed AWS API request using clients' AWS IAM credentials. IAM 396principals can then be assigned to roles within Vault. This is known as the 397"iam" auth method. 398 399Authentication of EC2 instances is done using either a signed PKCS#7 document 400or a detached RSA signature of an AWS EC2 instance's identity document along 401with a client-created nonce. This is known as the "ec2" auth method. 402 403If there is need to further restrict the capabilities of the role on the instance 404that is using the role, 'role_tag' option can be enabled on the role, and a tag 405can be generated using 'role/<role>/tag' endpoint. This tag represents the 406subset of capabilities set on the role. When the 'role_tag' option is enabled on 407the role, the login operation requires that a respective role tag is attached to 408the EC2 instance which performs the login. 409` 410