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