mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-14 02:27:02 +02:00
Add static roles to the aws secrets engine --------- Co-authored-by: maxcoulombe <max.coulombe@hashicorp.com> Co-authored-by: vinay-gopalan <86625824+vinay-gopalan@users.noreply.github.com> Co-authored-by: Yoko Hyakuna <yoko@hashicorp.com>
189 lines
6.2 KiB
Go
189 lines
6.2 KiB
Go
package aws
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/hashicorp/vault/sdk/queue"
|
|
)
|
|
|
|
// rotateExpiredStaticCreds will pop expired credentials (credentials whose priority
|
|
// represents a time before the present), rotate the associated credential, and push
|
|
// them back onto the queue with the new priority.
|
|
func (b *backend) rotateExpiredStaticCreds(ctx context.Context, req *logical.Request) error {
|
|
var errs *multierror.Error
|
|
|
|
for {
|
|
keepGoing, err := b.rotateCredential(ctx, req.Storage)
|
|
if err != nil {
|
|
errs = multierror.Append(errs, err)
|
|
}
|
|
if !keepGoing {
|
|
if errs.ErrorOrNil() != nil {
|
|
return fmt.Errorf("error(s) occurred while rotating expired static credentials: %w", errs)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// rotateCredential pops an element from the priority queue, and if it is expired, rotate and re-push.
|
|
// If a cred was rotated, it returns true, otherwise false.
|
|
func (b *backend) rotateCredential(ctx context.Context, storage logical.Storage) (rotated bool, err error) {
|
|
// If queue is empty or first item does not need a rotation (priority is next rotation timestamp) there is nothing to do
|
|
item, err := b.credRotationQueue.Pop()
|
|
if err != nil {
|
|
// the queue is just empty, which is fine.
|
|
if err == queue.ErrEmpty {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("failed to pop from queue for role %q: %w", item.Key, err)
|
|
}
|
|
if item.Priority > time.Now().Unix() {
|
|
// no rotation required
|
|
// push the item back into priority queue
|
|
err = b.credRotationQueue.Push(item)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to add item into the rotation queue for role %q: %w", item.Key, err)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
cfg := item.Value.(staticRoleEntry)
|
|
|
|
err = b.createCredential(ctx, storage, cfg, true)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// set new priority and re-queue
|
|
item.Priority = time.Now().Add(cfg.RotationPeriod).Unix()
|
|
err = b.credRotationQueue.Push(item)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to add item into the rotation queue for role %q: %w", cfg.Name, err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// createCredential will create a new iam credential, deleting the oldest one if necessary.
|
|
func (b *backend) createCredential(ctx context.Context, storage logical.Storage, cfg staticRoleEntry, shouldLockStorage bool) error {
|
|
iamClient, err := b.clientIAM(ctx, storage)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get the AWS IAM client: %w", err)
|
|
}
|
|
|
|
// IAM users can have a most 2 sets of keys at a time.
|
|
// (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html)
|
|
// Ideally we would get this value through an api check, but I'm not sure one exists.
|
|
const maxAllowedKeys = 2
|
|
|
|
err = b.validateIAMUserExists(ctx, storage, &cfg, false)
|
|
if err != nil {
|
|
return fmt.Errorf("iam user didn't exist, or username/userid didn't match: %w", err)
|
|
}
|
|
|
|
accessKeys, err := iamClient.ListAccessKeys(&iam.ListAccessKeysInput{
|
|
UserName: aws.String(cfg.Username),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to list existing access keys for IAM user %q: %w", cfg.Username, err)
|
|
}
|
|
|
|
// If we have the maximum number of keys, we have to delete one to make another (so we can get the credentials).
|
|
// We'll delete the oldest one.
|
|
//
|
|
// Since this check relies on a pre-coded maximum, it's a bit fragile. If the number goes up, we risk deleting
|
|
// a key when we didn't need to. If this number goes down, we'll start throwing errors because we think we're
|
|
// allowed to create a key and aren't. In either case, adjusting the constant should be sufficient to fix things.
|
|
if len(accessKeys.AccessKeyMetadata) >= maxAllowedKeys {
|
|
oldestKey := accessKeys.AccessKeyMetadata[0]
|
|
|
|
for i := 1; i < len(accessKeys.AccessKeyMetadata); i++ {
|
|
if accessKeys.AccessKeyMetadata[i].CreateDate.Before(*oldestKey.CreateDate) {
|
|
oldestKey = accessKeys.AccessKeyMetadata[i]
|
|
}
|
|
}
|
|
|
|
_, err := iamClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
|
|
AccessKeyId: oldestKey.AccessKeyId,
|
|
UserName: oldestKey.UserName,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete oldest access keys for user %q: %w", cfg.Username, err)
|
|
}
|
|
}
|
|
|
|
// Create new set of keys
|
|
out, err := iamClient.CreateAccessKey(&iam.CreateAccessKeyInput{
|
|
UserName: aws.String(cfg.Username),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create new access keys for user %q: %w", cfg.Username, err)
|
|
}
|
|
|
|
// Persist new keys
|
|
entry, err := logical.StorageEntryJSON(formatCredsStoragePath(cfg.Name), &awsCredentials{
|
|
AccessKeyID: *out.AccessKey.AccessKeyId,
|
|
SecretAccessKey: *out.AccessKey.SecretAccessKey,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal object to JSON: %w", err)
|
|
}
|
|
if shouldLockStorage {
|
|
b.roleMutex.Lock()
|
|
defer b.roleMutex.Unlock()
|
|
}
|
|
err = storage.Put(ctx, entry)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save object in storage: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// delete credential will remove the credential associated with the role from storage.
|
|
func (b *backend) deleteCredential(ctx context.Context, storage logical.Storage, cfg staticRoleEntry, shouldLockStorage bool) error {
|
|
// synchronize storage access if we didn't in the caller.
|
|
if shouldLockStorage {
|
|
b.roleMutex.Lock()
|
|
defer b.roleMutex.Unlock()
|
|
}
|
|
|
|
key, err := storage.Get(ctx, formatCredsStoragePath(cfg.Name))
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't find key in storage: %w", err)
|
|
}
|
|
// no entry, so i guess we deleted it already
|
|
if key == nil {
|
|
return nil
|
|
}
|
|
var creds awsCredentials
|
|
err = key.DecodeJSON(&creds)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't decode storage entry to a valid credential: %w", err)
|
|
}
|
|
|
|
err = storage.Delete(ctx, formatCredsStoragePath(cfg.Name))
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't delete from storage: %w", err)
|
|
}
|
|
|
|
// because we have the information, this is the one we created, so it's safe for us to delete.
|
|
_, err = b.iamClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
|
|
AccessKeyId: aws.String(creds.AccessKeyID),
|
|
UserName: aws.String(cfg.Username),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't delete from IAM: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|