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(conf *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			pathLogin(b),
122			pathListRole(b),
123			pathListRoles(b),
124			pathRole(b),
125			pathRoleTag(b),
126			pathConfigClient(b),
127			pathConfigCertificate(b),
128			pathConfigIdentity(b),
129			pathConfigSts(b),
130			pathListSts(b),
131			pathConfigTidyRoletagBlacklist(b),
132			pathConfigTidyIdentityWhitelist(b),
133			pathListCertificates(b),
134			pathListRoletagBlacklist(b),
135			pathRoletagBlacklist(b),
136			pathTidyRoletagBlacklist(b),
137			pathListIdentityWhitelist(b),
138			pathIdentityWhitelist(b),
139			pathTidyIdentityWhitelist(b),
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			// safety_buffer defaults to 180 days for roletag blacklist
164			safety_buffer := 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 safety_buffer with the configured value
177				safety_buffer = tidyBlacklistConfigEntry.SafetyBuffer
178			}
179			// tidy role tags if explicitly not disabled
180			if !skipBlacklistTidy {
181				b.tidyBlacklistRoleTag(ctx, req, safety_buffer)
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