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