mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-14 18:47:01 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
527 lines
16 KiB
Go
527 lines
16 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package aws
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/helper/template"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/iam"
|
|
"github.com/aws/aws-sdk-go/service/sts"
|
|
"github.com/hashicorp/errwrap"
|
|
)
|
|
|
|
const (
|
|
secretAccessKeyType = "access_keys"
|
|
storageKey = "config/root"
|
|
)
|
|
|
|
func secretAccessKeys(b *backend) *framework.Secret {
|
|
return &framework.Secret{
|
|
Type: secretAccessKeyType,
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"access_key": {
|
|
Type: framework.TypeString,
|
|
Description: "Access Key",
|
|
},
|
|
|
|
"secret_key": {
|
|
Type: framework.TypeString,
|
|
Description: "Secret Key",
|
|
},
|
|
"security_token": {
|
|
Type: framework.TypeString,
|
|
Description: "Security Token",
|
|
},
|
|
},
|
|
|
|
Renew: b.secretAccessKeysRenew,
|
|
Revoke: b.secretAccessKeysRevoke,
|
|
}
|
|
}
|
|
|
|
func genUsername(displayName, policyName, userType, usernameTemplate string) (ret string, err error) {
|
|
switch userType {
|
|
case "iam_user", "assume_role":
|
|
// IAM users are capped at 64 chars
|
|
up, err := template.NewTemplate(template.Template(usernameTemplate))
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to initialize username template: %w", err)
|
|
}
|
|
|
|
um := UsernameMetadata{
|
|
Type: "IAM",
|
|
DisplayName: normalizeDisplayName(displayName),
|
|
PolicyName: normalizeDisplayName(policyName),
|
|
}
|
|
|
|
ret, err = up.Generate(um)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate username: %w", err)
|
|
}
|
|
// To prevent a custom template from exceeding IAM length limits
|
|
if len(ret) > 64 {
|
|
return "", fmt.Errorf("the username generated by the template exceeds the IAM username length limits of 64 chars")
|
|
}
|
|
case "sts":
|
|
up, err := template.NewTemplate(template.Template(usernameTemplate))
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to initialize username template: %w", err)
|
|
}
|
|
|
|
um := UsernameMetadata{
|
|
Type: "STS",
|
|
}
|
|
ret, err = up.Generate(um)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate username: %w", err)
|
|
}
|
|
// To prevent a custom template from exceeding STS length limits
|
|
if len(ret) > 32 {
|
|
return "", fmt.Errorf("the username generated by the template exceeds the STS username length limits of 32 chars")
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (b *backend) getFederationToken(ctx context.Context, s logical.Storage,
|
|
displayName, policyName, policy string, policyARNs []string,
|
|
iamGroups []string, lifeTimeInSeconds int64) (*logical.Response, error,
|
|
) {
|
|
groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(ctx, s, iamGroups)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
if groupPolicies != nil {
|
|
groupPolicies = append(groupPolicies, policy)
|
|
policy, err = combinePolicyDocuments(groupPolicies...)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
}
|
|
if len(groupPolicyARNs) > 0 {
|
|
policyARNs = append(policyARNs, groupPolicyARNs...)
|
|
}
|
|
|
|
stsClient, err := b.clientSTS(ctx, s)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
|
|
config, err := readConfig(ctx, s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read configuration: %w", err)
|
|
}
|
|
|
|
// Set as defaultUsernameTemplate if not provided
|
|
usernameTemplate := config.UsernameTemplate
|
|
if usernameTemplate == "" {
|
|
usernameTemplate = defaultUserNameTemplate
|
|
}
|
|
|
|
username, usernameError := genUsername(displayName, policyName, "sts", usernameTemplate)
|
|
// Send a 400 to Framework.OperationFunc Handler
|
|
if usernameError != nil {
|
|
return nil, usernameError
|
|
}
|
|
|
|
getTokenInput := &sts.GetFederationTokenInput{
|
|
Name: aws.String(username),
|
|
DurationSeconds: &lifeTimeInSeconds,
|
|
}
|
|
if len(policy) > 0 {
|
|
getTokenInput.Policy = aws.String(policy)
|
|
}
|
|
if len(policyARNs) > 0 {
|
|
getTokenInput.PolicyArns = convertPolicyARNs(policyARNs)
|
|
}
|
|
|
|
// If neither a policy document nor policy ARNs are specified, then GetFederationToken will
|
|
// return credentials equivalent to that of the Vault server itself. We probably don't want
|
|
// that by default; the behavior can be explicitly opted in to by associating the Vault role
|
|
// with a policy ARN or document that allows the appropriate permissions.
|
|
if policy == "" && len(policyARNs) == 0 {
|
|
return logical.ErrorResponse("must specify at least one of policy_arns or policy_document with %s credential_type", federationTokenCred), nil
|
|
}
|
|
|
|
tokenResp, err := stsClient.GetFederationTokenWithContext(ctx, getTokenInput)
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error generating STS keys: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
|
|
// While STS credentials cannot be revoked/renewed, we will still create a lease since users are
|
|
// relying on a non-zero `lease_duration` in order to manage their lease lifecycles manually.
|
|
//
|
|
ttl := tokenResp.Credentials.Expiration.Sub(time.Now())
|
|
resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
|
|
"access_key": *tokenResp.Credentials.AccessKeyId,
|
|
"secret_key": *tokenResp.Credentials.SecretAccessKey,
|
|
"security_token": *tokenResp.Credentials.SessionToken,
|
|
"ttl": uint64(ttl.Seconds()),
|
|
}, map[string]interface{}{
|
|
"username": username,
|
|
"policy": policy,
|
|
"is_sts": true,
|
|
})
|
|
|
|
// Set the secret TTL to appropriately match the expiration of the token
|
|
resp.Secret.TTL = ttl
|
|
|
|
// STS are purposefully short-lived and aren't renewable
|
|
resp.Secret.Renewable = false
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (b *backend) assumeRole(ctx context.Context, s logical.Storage,
|
|
displayName, roleName, roleArn, policy string, policyARNs []string,
|
|
iamGroups []string, lifeTimeInSeconds int64, roleSessionName string) (*logical.Response, error,
|
|
) {
|
|
// grab any IAM group policies associated with the vault role, both inline
|
|
// and managed
|
|
groupPolicies, groupPolicyARNs, err := b.getGroupPolicies(ctx, s, iamGroups)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
if len(groupPolicies) > 0 {
|
|
groupPolicies = append(groupPolicies, policy)
|
|
policy, err = combinePolicyDocuments(groupPolicies...)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
}
|
|
if len(groupPolicyARNs) > 0 {
|
|
policyARNs = append(policyARNs, groupPolicyARNs...)
|
|
}
|
|
|
|
stsClient, err := b.clientSTS(ctx, s)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
|
|
config, err := readConfig(ctx, s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read configuration: %w", err)
|
|
}
|
|
|
|
// Set as defaultUsernameTemplate if not provided
|
|
usernameTemplate := config.UsernameTemplate
|
|
if usernameTemplate == "" {
|
|
usernameTemplate = defaultUserNameTemplate
|
|
}
|
|
|
|
var roleSessionNameError error
|
|
if roleSessionName == "" {
|
|
roleSessionName, roleSessionNameError = genUsername(displayName, roleName, "assume_role", usernameTemplate)
|
|
// Send a 400 to Framework.OperationFunc Handler
|
|
if roleSessionNameError != nil {
|
|
return nil, roleSessionNameError
|
|
}
|
|
} else {
|
|
roleSessionName = normalizeDisplayName(roleSessionName)
|
|
}
|
|
|
|
assumeRoleInput := &sts.AssumeRoleInput{
|
|
RoleSessionName: aws.String(roleSessionName),
|
|
RoleArn: aws.String(roleArn),
|
|
DurationSeconds: &lifeTimeInSeconds,
|
|
}
|
|
if policy != "" {
|
|
assumeRoleInput.SetPolicy(policy)
|
|
}
|
|
if len(policyARNs) > 0 {
|
|
assumeRoleInput.SetPolicyArns(convertPolicyARNs(policyARNs))
|
|
}
|
|
tokenResp, err := stsClient.AssumeRoleWithContext(ctx, assumeRoleInput)
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error assuming role: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
|
|
// While STS credentials cannot be revoked/renewed, we will still create a lease since users are
|
|
// relying on a non-zero `lease_duration` in order to manage their lease lifecycles manually.
|
|
//
|
|
ttl := tokenResp.Credentials.Expiration.Sub(time.Now())
|
|
resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
|
|
"access_key": *tokenResp.Credentials.AccessKeyId,
|
|
"secret_key": *tokenResp.Credentials.SecretAccessKey,
|
|
"security_token": *tokenResp.Credentials.SessionToken,
|
|
"arn": *tokenResp.AssumedRoleUser.Arn,
|
|
"ttl": uint64(ttl.Seconds()),
|
|
}, map[string]interface{}{
|
|
"username": roleSessionName,
|
|
"policy": roleArn,
|
|
"is_sts": true,
|
|
})
|
|
|
|
// Set the secret TTL to appropriately match the expiration of the token
|
|
resp.Secret.TTL = ttl
|
|
|
|
// STS are purposefully short-lived and aren't renewable
|
|
resp.Secret.Renewable = false
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func readConfig(ctx context.Context, storage logical.Storage) (rootConfig, error) {
|
|
entry, err := storage.Get(ctx, storageKey)
|
|
if err != nil {
|
|
return rootConfig{}, err
|
|
}
|
|
if entry == nil {
|
|
return rootConfig{}, nil
|
|
}
|
|
|
|
var connConfig rootConfig
|
|
if err := entry.DecodeJSON(&connConfig); err != nil {
|
|
return rootConfig{}, err
|
|
}
|
|
return connConfig, nil
|
|
}
|
|
|
|
func (b *backend) secretAccessKeysCreate(
|
|
ctx context.Context,
|
|
s logical.Storage,
|
|
displayName, policyName string,
|
|
role *awsRoleEntry,
|
|
) (*logical.Response, error) {
|
|
iamClient, err := b.clientIAM(ctx, s)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
|
|
config, err := readConfig(ctx, s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read configuration: %w", err)
|
|
}
|
|
|
|
// Set as defaultUsernameTemplate if not provided
|
|
usernameTemplate := config.UsernameTemplate
|
|
if usernameTemplate == "" {
|
|
usernameTemplate = defaultUserNameTemplate
|
|
}
|
|
|
|
username, usernameError := genUsername(displayName, policyName, "iam_user", usernameTemplate)
|
|
// Send a 400 to Framework.OperationFunc Handler
|
|
if usernameError != nil {
|
|
return nil, usernameError
|
|
}
|
|
|
|
// Write to the WAL that this user will be created. We do this before
|
|
// the user is created because if switch the order then the WAL put
|
|
// can fail, which would put us in an awkward position: we have a user
|
|
// we need to rollback but can't put the WAL entry to do the rollback.
|
|
walID, err := framework.PutWAL(ctx, s, "user", &walUser{
|
|
UserName: username,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error writing WAL entry: %w", err)
|
|
}
|
|
|
|
userPath := role.UserPath
|
|
if userPath == "" {
|
|
userPath = "/"
|
|
}
|
|
|
|
createUserRequest := &iam.CreateUserInput{
|
|
UserName: aws.String(username),
|
|
Path: aws.String(userPath),
|
|
}
|
|
if role.PermissionsBoundaryARN != "" {
|
|
createUserRequest.PermissionsBoundary = aws.String(role.PermissionsBoundaryARN)
|
|
}
|
|
|
|
// Create the user
|
|
_, err = iamClient.CreateUserWithContext(ctx, createUserRequest)
|
|
if err != nil {
|
|
if walErr := framework.DeleteWAL(ctx, s, walID); walErr != nil {
|
|
iamErr := fmt.Errorf("error creating IAM user: %w", err)
|
|
return nil, errwrap.Wrap(fmt.Errorf("failed to delete WAL entry: %w", walErr), iamErr)
|
|
}
|
|
return logical.ErrorResponse("Error creating IAM user: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
|
|
for _, arn := range role.PolicyArns {
|
|
// Attach existing policy against user
|
|
_, err = iamClient.AttachUserPolicyWithContext(ctx, &iam.AttachUserPolicyInput{
|
|
UserName: aws.String(username),
|
|
PolicyArn: aws.String(arn),
|
|
})
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error attaching user policy: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
|
|
}
|
|
if role.PolicyDocument != "" {
|
|
// Add new inline user policy against user
|
|
_, err = iamClient.PutUserPolicyWithContext(ctx, &iam.PutUserPolicyInput{
|
|
UserName: aws.String(username),
|
|
PolicyName: aws.String(policyName),
|
|
PolicyDocument: aws.String(role.PolicyDocument),
|
|
})
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error putting user policy: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
}
|
|
|
|
for _, group := range role.IAMGroups {
|
|
// Add user to IAM groups
|
|
_, err = iamClient.AddUserToGroupWithContext(ctx, &iam.AddUserToGroupInput{
|
|
UserName: aws.String(username),
|
|
GroupName: aws.String(group),
|
|
})
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error adding user to group: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
}
|
|
|
|
var tags []*iam.Tag
|
|
for key, value := range role.IAMTags {
|
|
// This assignment needs to be done in order to create unique addresses for
|
|
// these variables. Without doing so, all the tags will be copies of the last
|
|
// tag listed in the role.
|
|
k, v := key, value
|
|
tags = append(tags, &iam.Tag{Key: &k, Value: &v})
|
|
}
|
|
|
|
if len(tags) > 0 {
|
|
_, err = iamClient.TagUserWithContext(ctx, &iam.TagUserInput{
|
|
Tags: tags,
|
|
UserName: &username,
|
|
})
|
|
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error adding tags to user: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
}
|
|
|
|
// Create the keys
|
|
keyResp, err := iamClient.CreateAccessKeyWithContext(ctx, &iam.CreateAccessKeyInput{
|
|
UserName: aws.String(username),
|
|
})
|
|
if err != nil {
|
|
return logical.ErrorResponse("Error creating access keys: %s", err), awsutil.CheckAWSError(err)
|
|
}
|
|
|
|
// Remove the WAL entry, we succeeded! If we fail, we don't return
|
|
// the secret because it'll get rolled back anyways, so we have to return
|
|
// an error here.
|
|
if err := framework.DeleteWAL(ctx, s, walID); err != nil {
|
|
return nil, fmt.Errorf("failed to commit WAL entry: %w", err)
|
|
}
|
|
|
|
// Return the info!
|
|
resp := b.Secret(secretAccessKeyType).Response(map[string]interface{}{
|
|
"access_key": *keyResp.AccessKey.AccessKeyId,
|
|
"secret_key": *keyResp.AccessKey.SecretAccessKey,
|
|
"security_token": nil,
|
|
}, map[string]interface{}{
|
|
"username": username,
|
|
"policy": role,
|
|
"is_sts": false,
|
|
})
|
|
|
|
lease, err := b.Lease(ctx, s)
|
|
if err != nil || lease == nil {
|
|
lease = &configLease{}
|
|
}
|
|
|
|
resp.Secret.TTL = lease.Lease
|
|
resp.Secret.MaxTTL = lease.LeaseMax
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (b *backend) secretAccessKeysRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
// STS already has a lifetime, and we don't support renewing it
|
|
isSTSRaw, ok := req.Secret.InternalData["is_sts"]
|
|
if ok {
|
|
isSTS, ok := isSTSRaw.(bool)
|
|
if ok {
|
|
if isSTS {
|
|
return nil, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
lease, err := b.Lease(ctx, req.Storage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lease == nil {
|
|
lease = &configLease{}
|
|
}
|
|
|
|
resp := &logical.Response{Secret: req.Secret}
|
|
resp.Secret.TTL = lease.Lease
|
|
resp.Secret.MaxTTL = lease.LeaseMax
|
|
return resp, nil
|
|
}
|
|
|
|
func (b *backend) secretAccessKeysRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
// STS cleans up after itself so we can skip this if is_sts internal data
|
|
// element set to true. If is_sts is not set, assumes old version
|
|
// and defaults to the IAM approach.
|
|
isSTSRaw, ok := req.Secret.InternalData["is_sts"]
|
|
if ok {
|
|
isSTS, ok := isSTSRaw.(bool)
|
|
if ok {
|
|
if isSTS {
|
|
return nil, nil
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("secret has is_sts but value could not be understood")
|
|
}
|
|
}
|
|
|
|
// Get the username from the internal data
|
|
usernameRaw, ok := req.Secret.InternalData["username"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("secret is missing username internal data")
|
|
}
|
|
username, ok := usernameRaw.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("secret is missing username internal data")
|
|
}
|
|
|
|
// Use the user rollback mechanism to delete this user
|
|
err := b.pathUserRollback(ctx, req, "user", map[string]interface{}{
|
|
"username": username,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func normalizeDisplayName(displayName string) string {
|
|
re := regexp.MustCompile("[^a-zA-Z0-9+=,.@_-]")
|
|
return re.ReplaceAllString(displayName, "_")
|
|
}
|
|
|
|
func convertPolicyARNs(policyARNs []string) []*sts.PolicyDescriptorType {
|
|
size := len(policyARNs)
|
|
retval := make([]*sts.PolicyDescriptorType, size, size)
|
|
for i, arn := range policyARNs {
|
|
retval[i] = &sts.PolicyDescriptorType{
|
|
Arn: aws.String(arn),
|
|
}
|
|
}
|
|
return retval
|
|
}
|
|
|
|
type UsernameMetadata struct {
|
|
Type string
|
|
DisplayName string
|
|
PolicyName string
|
|
}
|