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