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/helper/awsutil" 14 "github.com/hashicorp/vault/sdk/framework" 15 "github.com/hashicorp/vault/sdk/helper/consts" 16 "github.com/hashicorp/vault/sdk/logical" 17 cache "github.com/patrickmn/go-cache" 18) 19 20func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 21 b, err := Backend(conf) 22 if err != nil { 23 return nil, err 24 } 25 if err := b.Setup(ctx, conf); err != nil { 26 return nil, err 27 } 28 return b, nil 29} 30 31type backend struct { 32 *framework.Backend 33 34 // Lock to make changes to any of the backend's configuration endpoints. 35 configMutex sync.RWMutex 36 37 // Lock to make changes to role entries 38 roleMutex sync.Mutex 39 40 // Lock to make changes to the blacklist entries 41 blacklistMutex sync.RWMutex 42 43 // Guards the blacklist/whitelist tidy functions 44 tidyBlacklistCASGuard *uint32 45 tidyWhitelistCASGuard *uint32 46 47 // Duration after which the periodic function of the backend needs to 48 // tidy the blacklist and whitelist entries. 49 tidyCooldownPeriod time.Duration 50 51 // nextTidyTime holds the time at which the periodic func should initiate 52 // the tidy operations. This is set by the periodicFunc based on the value 53 // of tidyCooldownPeriod. 54 nextTidyTime time.Time 55 56 // Map to hold the EC2 client objects indexed by region and STS role. 57 // This avoids the overhead of creating a client object for every login request. 58 // When the credentials are modified or deleted, all the cached client objects 59 // will be flushed. The empty STS role signifies the master account 60 EC2ClientsMap map[string]map[string]*ec2.EC2 61 62 // Map to hold the IAM client objects indexed by region and STS role. 63 // This avoids the overhead of creating a client object for every login request. 64 // When the credentials are modified or deleted, all the cached client objects 65 // will be flushed. The empty STS role signifies the master account 66 IAMClientsMap map[string]map[string]*iam.IAM 67 68 // Map of AWS unique IDs to the full ARN corresponding to that unique ID 69 // This avoids the overhead of an AWS API hit for every login request 70 // using the IAM auth method when bound_iam_principal_arn contains a wildcard 71 iamUserIdToArnCache *cache.Cache 72 73 // AWS Account ID of the "default" AWS credentials 74 // This cache avoids the need to call GetCallerIdentity repeatedly to learn it 75 // We can't store this because, in certain pathological cases, it could change 76 // out from under us, such as a standby and active Vault server in different AWS 77 // accounts using their IAM instance profile to get their credentials. 78 defaultAWSAccountID string 79 80 // roleCache caches role entries to avoid locking headaches 81 roleCache *cache.Cache 82 83 resolveArnToUniqueIDFunc func(context.Context, logical.Storage, string) (string, error) 84 85 // upgradeCancelFunc is used to cancel the context used in the upgrade 86 // function 87 upgradeCancelFunc context.CancelFunc 88} 89 90func Backend(_ *logical.BackendConfig) (*backend, error) { 91 b := &backend{ 92 // Setting the periodic func to be run once in an hour. 93 // If there is a real need, this can be made configurable. 94 tidyCooldownPeriod: time.Hour, 95 EC2ClientsMap: make(map[string]map[string]*ec2.EC2), 96 IAMClientsMap: make(map[string]map[string]*iam.IAM), 97 iamUserIdToArnCache: cache.New(7*24*time.Hour, 24*time.Hour), 98 tidyBlacklistCASGuard: new(uint32), 99 tidyWhitelistCASGuard: new(uint32), 100 roleCache: cache.New(cache.NoExpiration, cache.NoExpiration), 101 } 102 103 b.resolveArnToUniqueIDFunc = b.resolveArnToRealUniqueId 104 105 b.Backend = &framework.Backend{ 106 PeriodicFunc: b.periodicFunc, 107 AuthRenew: b.pathLoginRenew, 108 Help: backendHelp, 109 PathsSpecial: &logical.Paths{ 110 Unauthenticated: []string{ 111 "login", 112 }, 113 LocalStorage: []string{ 114 "whitelist/identity/", 115 }, 116 SealWrapStorage: []string{ 117 "config/client", 118 }, 119 }, 120 Paths: []*framework.Path{ 121 b.pathLogin(), 122 b.pathListRole(), 123 b.pathListRoles(), 124 b.pathRole(), 125 b.pathRoleTag(), 126 b.pathConfigClient(), 127 b.pathConfigCertificate(), 128 b.pathConfigIdentity(), 129 b.pathConfigSts(), 130 b.pathListSts(), 131 b.pathConfigTidyRoletagBlacklist(), 132 b.pathConfigTidyIdentityWhitelist(), 133 b.pathListCertificates(), 134 b.pathListRoletagBlacklist(), 135 b.pathRoletagBlacklist(), 136 b.pathTidyRoletagBlacklist(), 137 b.pathListIdentityWhitelist(), 138 b.pathIdentityWhitelist(), 139 b.pathTidyIdentityWhitelist(), 140 }, 141 Invalidate: b.invalidate, 142 InitializeFunc: b.initialize, 143 BackendType: logical.TypeCredential, 144 Clean: b.cleanup, 145 } 146 147 return b, nil 148} 149 150// periodicFunc performs the tasks that the backend wishes to do periodically. 151// Currently this will be triggered once in a minute by the RollbackManager. 152// 153// The tasks being done currently by this function are to cleanup the expired 154// entries of both blacklist role tags and whitelist identities. Tidying is done 155// not once in a minute, but once in an hour, controlled by 'tidyCooldownPeriod'. 156// Tidying of blacklist and whitelist are by default enabled. This can be 157// changed using `config/tidy/roletags` and `config/tidy/identities` endpoints. 158func (b *backend) periodicFunc(ctx context.Context, req *logical.Request) error { 159 // Run the tidy operations for the first time. Then run it when current 160 // time matches the nextTidyTime. 161 if b.nextTidyTime.IsZero() || !time.Now().Before(b.nextTidyTime) { 162 if b.System().LocalMount() || !b.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary|consts.ReplicationPerformanceStandby) { 163 // safetyBuffer defaults to 180 days for roletag blacklist 164 safetyBuffer := 15552000 165 tidyBlacklistConfigEntry, err := b.lockedConfigTidyRoleTags(ctx, req.Storage) 166 if err != nil { 167 return err 168 } 169 skipBlacklistTidy := false 170 // check if tidying of role tags was configured 171 if tidyBlacklistConfigEntry != nil { 172 // check if periodic tidying of role tags was disabled 173 if tidyBlacklistConfigEntry.DisablePeriodicTidy { 174 skipBlacklistTidy = true 175 } 176 // overwrite the default safetyBuffer with the configured value 177 safetyBuffer = tidyBlacklistConfigEntry.SafetyBuffer 178 } 179 // tidy role tags if explicitly not disabled 180 if !skipBlacklistTidy { 181 b.tidyBlacklistRoleTag(ctx, req, safetyBuffer) 182 } 183 } 184 185 // We don't check for replication state for whitelist identities as 186 // these are locally stored 187 188 safety_buffer := 259200 189 tidyWhitelistConfigEntry, err := b.lockedConfigTidyIdentities(ctx, req.Storage) 190 if err != nil { 191 return err 192 } 193 skipWhitelistTidy := false 194 // check if tidying of identities was configured 195 if tidyWhitelistConfigEntry != nil { 196 // check if periodic tidying of identities was disabled 197 if tidyWhitelistConfigEntry.DisablePeriodicTidy { 198 skipWhitelistTidy = true 199 } 200 // overwrite the default safety_buffer with the configured value 201 safety_buffer = tidyWhitelistConfigEntry.SafetyBuffer 202 } 203 // tidy identities if explicitly not disabled 204 if !skipWhitelistTidy { 205 b.tidyWhitelistIdentity(ctx, req, safety_buffer) 206 } 207 208 // Update the time at which to run the tidy functions again. 209 b.nextTidyTime = time.Now().Add(b.tidyCooldownPeriod) 210 } 211 return nil 212} 213 214func (b *backend) cleanup(ctx context.Context) { 215 if b.upgradeCancelFunc != nil { 216 b.upgradeCancelFunc() 217 } 218} 219 220func (b *backend) invalidate(ctx context.Context, key string) { 221 switch { 222 case key == "config/client": 223 b.configMutex.Lock() 224 defer b.configMutex.Unlock() 225 b.flushCachedEC2Clients() 226 b.flushCachedIAMClients() 227 b.defaultAWSAccountID = "" 228 case strings.HasPrefix(key, "role"): 229 // TODO: We could make this better 230 b.roleCache.Flush() 231 } 232} 233 234// Putting this here so we can inject a fake resolver into the backend for unit testing 235// purposes 236func (b *backend) resolveArnToRealUniqueId(ctx context.Context, s logical.Storage, arn string) (string, error) { 237 entity, err := parseIamArn(arn) 238 if err != nil { 239 return "", err 240 } 241 // This odd-looking code is here because IAM is an inherently global service. IAM and STS ARNs 242 // don't have regions in them, and there is only a single global endpoint for IAM; see 243 // http://docs.aws.amazon.com/general/latest/gr/rande.html#iam_region 244 // However, the ARNs do have a partition in them, because the GovCloud and China partitions DO 245 // have their own separate endpoints, and the partition is encoded in the ARN. If Amazon's Go SDK 246 // would allow us to pass a partition back to the IAM client, it would be much simpler. But it 247 // doesn't appear that's possible, so in order to properly support GovCloud and China, we do a 248 // circular dance of extracting the partition from the ARN, finding any arbitrary region in the 249 // partition, and passing that region back back to the SDK, so that the SDK can figure out the 250 // proper partition from the arbitrary region we passed in to look up the endpoint. 251 // Sigh 252 region := getAnyRegionForAwsPartition(entity.Partition) 253 if region == nil { 254 return "", fmt.Errorf("unable to resolve partition %q to a region", entity.Partition) 255 } 256 iamClient, err := b.clientIAM(ctx, s, region.ID(), entity.AccountNumber) 257 if err != nil { 258 return "", awsutil.AppendLogicalError(err) 259 } 260 261 switch entity.Type { 262 case "user": 263 userInfo, err := iamClient.GetUser(&iam.GetUserInput{UserName: &entity.FriendlyName}) 264 if err != nil { 265 return "", awsutil.AppendLogicalError(err) 266 } 267 if userInfo == nil { 268 return "", fmt.Errorf("got nil result from GetUser") 269 } 270 return *userInfo.User.UserId, nil 271 case "role": 272 roleInfo, err := iamClient.GetRole(&iam.GetRoleInput{RoleName: &entity.FriendlyName}) 273 if err != nil { 274 return "", awsutil.AppendLogicalError(err) 275 } 276 if roleInfo == nil { 277 return "", fmt.Errorf("got nil result from GetRole") 278 } 279 return *roleInfo.Role.RoleId, nil 280 case "instance-profile": 281 profileInfo, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{InstanceProfileName: &entity.FriendlyName}) 282 if err != nil { 283 return "", awsutil.AppendLogicalError(err) 284 } 285 if profileInfo == nil { 286 return "", fmt.Errorf("got nil result from GetInstanceProfile") 287 } 288 return *profileInfo.InstanceProfile.InstanceProfileId, nil 289 default: 290 return "", fmt.Errorf("unrecognized error type %#v", entity.Type) 291 } 292} 293 294// Adapted from https://docs.aws.amazon.com/sdk-for-go/api/aws/endpoints/ 295// the "Enumerating Regions and Endpoint Metadata" section 296func getAnyRegionForAwsPartition(partitionId string) *endpoints.Region { 297 resolver := endpoints.DefaultResolver() 298 partitions := resolver.(endpoints.EnumPartitions).Partitions() 299 300 for _, p := range partitions { 301 if p.ID() == partitionId { 302 for _, r := range p.Regions() { 303 return &r 304 } 305 } 306 } 307 return nil 308} 309 310const backendHelp = ` 311The aws auth method uses either AWS IAM credentials or AWS-signed EC2 metadata 312to authenticate clients, which are IAM principals or EC2 instances. 313 314Authentication is backed by a preconfigured role in the backend. The role 315represents the authorization of resources by containing Vault's policies. 316Role can be created using 'role/<role>' endpoint. 317 318Authentication of IAM principals, either IAM users or roles, is done using a 319specifically signed AWS API request using clients' AWS IAM credentials. IAM 320principals can then be assigned to roles within Vault. This is known as the 321"iam" auth method. 322 323Authentication of EC2 instances is done using either a signed PKCS#7 document 324or a detached RSA signature of an AWS EC2 instance's identity document along 325with a client-created nonce. This is known as the "ec2" auth method. 326 327If there is need to further restrict the capabilities of the role on the instance 328that is using the role, 'role_tag' option can be enabled on the role, and a tag 329can be generated using 'role/<role>/tag' endpoint. This tag represents the 330subset of capabilities set on the role. When the 'role_tag' option is enabled on 331the role, the login operation requires that a respective role tag is attached to 332the EC2 instance which performs the login. 333` 334