mirror of
https://github.com/hashicorp/vault.git
synced 2025-11-17 16:51:45 +01: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>
446 lines
15 KiB
Go
446 lines
15 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package approle
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
uuid "github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/vault/helper/parseip"
|
|
"github.com/hashicorp/vault/sdk/helper/cidrutil"
|
|
"github.com/hashicorp/vault/sdk/helper/locksutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
// secretIDStorageEntry represents the information stored in storage
|
|
// when a SecretID is created. The structure of the SecretID storage
|
|
// entry is the same for all the types of SecretIDs generated.
|
|
type secretIDStorageEntry struct {
|
|
// Accessor for the SecretID. It is a random UUID serving as
|
|
// a secondary index for the SecretID. This uniquely identifies
|
|
// the SecretID it belongs to, and hence can be used for listing
|
|
// and deleting SecretIDs. Accessors cannot be used as valid
|
|
// SecretIDs during login.
|
|
SecretIDAccessor string `json:"secret_id_accessor" mapstructure:"secret_id_accessor"`
|
|
|
|
// Number of times this SecretID can be used to perform the login
|
|
// operation
|
|
SecretIDNumUses int `json:"secret_id_num_uses" mapstructure:"secret_id_num_uses"`
|
|
|
|
// Duration after which this SecretID should expire. This is capped by
|
|
// the backend mount's max TTL value.
|
|
SecretIDTTL time.Duration `json:"secret_id_ttl" mapstructure:"secret_id_ttl"`
|
|
|
|
// The time when the SecretID was created
|
|
CreationTime time.Time `json:"creation_time" mapstructure:"creation_time"`
|
|
|
|
// The time when the SecretID becomes eligible for tidy operation.
|
|
// Tidying is performed by the PeriodicFunc of the backend which is 1
|
|
// minute apart.
|
|
ExpirationTime time.Time `json:"expiration_time" mapstructure:"expiration_time"`
|
|
|
|
// The time representing the last time this storage entry was modified
|
|
LastUpdatedTime time.Time `json:"last_updated_time" mapstructure:"last_updated_time"`
|
|
|
|
// Metadata that belongs to the SecretID
|
|
Metadata map[string]string `json:"metadata" mapstructure:"metadata"`
|
|
|
|
// CIDRList is a set of CIDR blocks that impose source address
|
|
// restrictions on the usage of SecretID
|
|
CIDRList []string `json:"cidr_list" mapstructure:"cidr_list"`
|
|
|
|
// TokenBoundCIDRs is a set of CIDR blocks that impose source address
|
|
// restrictions on the usage of the token generated by this SecretID
|
|
TokenBoundCIDRs []string `json:"token_cidr_list" mapstructure:"token_bound_cidrs"`
|
|
|
|
// This is a deprecated field
|
|
SecretIDNumUsesDeprecated int `json:"SecretIDNumUses" mapstructure:"SecretIDNumUses"`
|
|
}
|
|
|
|
// Represents the payload of the storage entry of the accessor that maps to a
|
|
// unique SecretID. Note that SecretIDs should never be stored in plaintext
|
|
// anywhere in the backend. SecretIDHMAC will be used as an index to fetch the
|
|
// properties of the SecretID and to delete the SecretID.
|
|
type secretIDAccessorStorageEntry struct {
|
|
// Hash of the SecretID which can be used to find the storage index at which
|
|
// properties of SecretID is stored.
|
|
SecretIDHMAC string `json:"secret_id_hmac" mapstructure:"secret_id_hmac"`
|
|
}
|
|
|
|
// verifyCIDRRoleSecretIDSubset checks if the CIDR blocks set on the secret ID
|
|
// are a subset of CIDR blocks set on the role
|
|
func verifyCIDRRoleSecretIDSubset(secretIDCIDRs []string, roleBoundCIDRList []string) error {
|
|
if len(secretIDCIDRs) != 0 {
|
|
// If there are no CIDR blocks on the role, then the subset
|
|
// requirement would be satisfied
|
|
if len(roleBoundCIDRList) != 0 {
|
|
// Address blocks with /32 mask do not get stored with the CIDR mask
|
|
// Check if there are any /32 addresses and append CIDR mask
|
|
for i, block := range roleBoundCIDRList {
|
|
if !strings.Contains(block, "/") {
|
|
roleBoundCIDRList[i] = fmt.Sprint(block, "/32")
|
|
}
|
|
}
|
|
|
|
subset, err := cidrutil.SubsetBlocks(roleBoundCIDRList, secretIDCIDRs)
|
|
if !subset || err != nil {
|
|
return fmt.Errorf(
|
|
"failed to verify subset relationship between CIDR blocks on the role %q and CIDR blocks on the secret ID %q: %w",
|
|
roleBoundCIDRList,
|
|
secretIDCIDRs,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const maxHmacInputLength = 4096
|
|
|
|
// Creates a SHA256 HMAC of the given 'value' using the given 'key' and returns
|
|
// a hex encoded string.
|
|
func createHMAC(key, value string) (string, error) {
|
|
if key == "" {
|
|
return "", fmt.Errorf("invalid HMAC key")
|
|
}
|
|
|
|
if len(value) > maxHmacInputLength {
|
|
return "", fmt.Errorf("value is longer than maximum of %d bytes", maxHmacInputLength)
|
|
}
|
|
|
|
hm := hmac.New(sha256.New, []byte(key))
|
|
hm.Write([]byte(value))
|
|
return hex.EncodeToString(hm.Sum(nil)), nil
|
|
}
|
|
|
|
func (b *backend) secretIDLock(secretIDHMAC string) *locksutil.LockEntry {
|
|
return locksutil.LockForKey(b.secretIDLocks, secretIDHMAC)
|
|
}
|
|
|
|
func (b *backend) secretIDAccessorLock(secretIDAccessor string) *locksutil.LockEntry {
|
|
return locksutil.LockForKey(b.secretIDAccessorLocks, secretIDAccessor)
|
|
}
|
|
|
|
func decodeSecretIDStorageEntry(entry *logical.StorageEntry) (*secretIDStorageEntry, error) {
|
|
result := secretIDStorageEntry{}
|
|
if err := entry.DecodeJSON(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cleanup := func(in []string) []string {
|
|
if len(in) == 0 {
|
|
// Don't change unnecessarily, if it was empty list leave as empty list
|
|
// instead of making it nil.
|
|
return in
|
|
}
|
|
var out []string
|
|
for _, s := range in {
|
|
out = append(out, parseip.TrimLeadingZeroesCIDR(s))
|
|
}
|
|
return out
|
|
}
|
|
|
|
result.CIDRList = cleanup(result.CIDRList)
|
|
result.TokenBoundCIDRs = cleanup(result.TokenBoundCIDRs)
|
|
return &result, nil
|
|
}
|
|
|
|
// nonLockedSecretIDStorageEntry fetches the secret ID properties from physical
|
|
// storage. The entry will be indexed based on the given HMACs of both role
|
|
// name and the secret ID. This method will not acquire secret ID lock to fetch
|
|
// the storage entry. Locks need to be acquired before calling this method.
|
|
func (b *backend) nonLockedSecretIDStorageEntry(ctx context.Context, s logical.Storage, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC string) (*secretIDStorageEntry, error) {
|
|
if secretIDHMAC == "" {
|
|
return nil, fmt.Errorf("missing secret ID HMAC")
|
|
}
|
|
|
|
if roleNameHMAC == "" {
|
|
return nil, fmt.Errorf("missing role name HMAC")
|
|
}
|
|
|
|
// Prepare the storage index at which the secret ID will be stored
|
|
entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
|
|
|
|
entry, err := s.Get(ctx, entryIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
result, err := decodeSecretIDStorageEntry(entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: Remove this upgrade bit in future releases
|
|
persistNeeded := false
|
|
if result.SecretIDNumUsesDeprecated != 0 {
|
|
if result.SecretIDNumUses == 0 ||
|
|
result.SecretIDNumUsesDeprecated < result.SecretIDNumUses {
|
|
result.SecretIDNumUses = result.SecretIDNumUsesDeprecated
|
|
persistNeeded = true
|
|
}
|
|
if result.SecretIDNumUses < result.SecretIDNumUsesDeprecated {
|
|
result.SecretIDNumUsesDeprecated = result.SecretIDNumUses
|
|
persistNeeded = true
|
|
}
|
|
}
|
|
|
|
if persistNeeded {
|
|
if err := b.nonLockedSetSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC, result); err != nil {
|
|
return nil, fmt.Errorf("failed to upgrade role storage entry %w", err)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// nonLockedSetSecretIDStorageEntry creates or updates a secret ID entry at the
|
|
// physical storage. The entry will be indexed based on the given HMACs of both
|
|
// role name and the secret ID. This method will not acquire secret ID lock to
|
|
// create/update the storage entry. Locks need to be acquired before calling
|
|
// this method.
|
|
func (b *backend) nonLockedSetSecretIDStorageEntry(ctx context.Context, s logical.Storage, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC string, secretEntry *secretIDStorageEntry) error {
|
|
if roleSecretIDPrefix == "" {
|
|
return fmt.Errorf("missing secret ID prefix")
|
|
}
|
|
if secretIDHMAC == "" {
|
|
return fmt.Errorf("missing secret ID HMAC")
|
|
}
|
|
|
|
if roleNameHMAC == "" {
|
|
return fmt.Errorf("missing role name HMAC")
|
|
}
|
|
|
|
if secretEntry == nil {
|
|
return fmt.Errorf("nil secret entry")
|
|
}
|
|
|
|
entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
|
|
|
|
if entry, err := logical.StorageEntryJSON(entryIndex, secretEntry); err != nil {
|
|
return err
|
|
} else if err = s.Put(ctx, entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// registerSecretIDEntry creates a new storage entry for the given SecretID.
|
|
func (b *backend) registerSecretIDEntry(ctx context.Context, s logical.Storage, roleName, secretID, hmacKey, roleSecretIDPrefix string, secretEntry *secretIDStorageEntry) (*secretIDStorageEntry, error) {
|
|
secretIDHMAC, err := createHMAC(hmacKey, secretID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create HMAC of secret ID: %w", err)
|
|
}
|
|
roleNameHMAC, err := createHMAC(hmacKey, roleName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create HMAC of role_name: %w", err)
|
|
}
|
|
|
|
lock := b.secretIDLock(secretIDHMAC)
|
|
lock.RLock()
|
|
|
|
entry, err := b.nonLockedSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
|
|
if err != nil {
|
|
lock.RUnlock()
|
|
return nil, err
|
|
}
|
|
if entry != nil {
|
|
lock.RUnlock()
|
|
return nil, fmt.Errorf("SecretID is already registered")
|
|
}
|
|
|
|
// If there isn't an entry for the secretID already, switch the read lock
|
|
// with a write lock and create an entry.
|
|
lock.RUnlock()
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
// But before saving a new entry, check if the secretID entry was created during the lock switch.
|
|
entry, err = b.nonLockedSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if entry != nil {
|
|
return nil, fmt.Errorf("SecretID is already registered")
|
|
}
|
|
|
|
//
|
|
// Create a new entry for the SecretID
|
|
//
|
|
|
|
// Set the creation time for the SecretID
|
|
currentTime := time.Now()
|
|
secretEntry.CreationTime = currentTime
|
|
secretEntry.LastUpdatedTime = currentTime
|
|
|
|
if ttl := b.deriveSecretIDTTL(secretEntry.SecretIDTTL); ttl != time.Duration(0) {
|
|
secretEntry.ExpirationTime = currentTime.Add(ttl)
|
|
}
|
|
|
|
// Before storing the SecretID, store its accessor.
|
|
if err := b.createSecretIDAccessorEntry(ctx, s, secretEntry, secretIDHMAC, roleSecretIDPrefix); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := b.nonLockedSetSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC, secretEntry); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return secretEntry, nil
|
|
}
|
|
|
|
// deriveSecretIDTTL determines the secret ID TTL to use based on the system's
|
|
// max lease TTL.
|
|
//
|
|
// If SecretIDTTL is negative or if it crosses the backend mount's limit,
|
|
// return to backend's max lease TTL. Otherwise, return the provided secretIDTTL
|
|
// value.
|
|
func (b *backend) deriveSecretIDTTL(secretIDTTL time.Duration) time.Duration {
|
|
if secretIDTTL < time.Duration(0) || secretIDTTL > b.System().MaxLeaseTTL() {
|
|
return b.System().MaxLeaseTTL()
|
|
}
|
|
|
|
return secretIDTTL
|
|
}
|
|
|
|
// secretIDAccessorEntry is used to read the storage entry that maps an
|
|
// accessor to a secret_id.
|
|
func (b *backend) secretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) (*secretIDAccessorStorageEntry, error) {
|
|
if secretIDAccessor == "" {
|
|
return nil, fmt.Errorf("missing secretIDAccessor")
|
|
}
|
|
|
|
var result secretIDAccessorStorageEntry
|
|
|
|
// Create index entry, mapping the accessor to the token ID
|
|
salt, err := b.Salt(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accessorPrefix := secretIDAccessorPrefix
|
|
if roleSecretIDPrefix == secretIDLocalPrefix {
|
|
accessorPrefix = secretIDAccessorLocalPrefix
|
|
}
|
|
entryIndex := accessorPrefix + salt.SaltID(secretIDAccessor)
|
|
|
|
accessorLock := b.secretIDAccessorLock(secretIDAccessor)
|
|
accessorLock.RLock()
|
|
defer accessorLock.RUnlock()
|
|
|
|
if entry, err := s.Get(ctx, entryIndex); err != nil {
|
|
return nil, err
|
|
} else if entry == nil {
|
|
return nil, nil
|
|
} else if err := entry.DecodeJSON(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// createSecretIDAccessorEntry creates an identifier for the SecretID. A storage index,
|
|
// mapping the accessor to the SecretID is also created. This method should
|
|
// be called when the lock for the corresponding SecretID is held.
|
|
func (b *backend) createSecretIDAccessorEntry(ctx context.Context, s logical.Storage, entry *secretIDStorageEntry, secretIDHMAC, roleSecretIDPrefix string) error {
|
|
// Create a random accessor
|
|
accessorUUID, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry.SecretIDAccessor = accessorUUID
|
|
|
|
// Create index entry, mapping the accessor to the token ID
|
|
salt, err := b.Salt(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accessorPrefix := secretIDAccessorPrefix
|
|
if roleSecretIDPrefix == secretIDLocalPrefix {
|
|
accessorPrefix = secretIDAccessorLocalPrefix
|
|
}
|
|
entryIndex := accessorPrefix + salt.SaltID(entry.SecretIDAccessor)
|
|
|
|
accessorLock := b.secretIDAccessorLock(accessorUUID)
|
|
accessorLock.Lock()
|
|
defer accessorLock.Unlock()
|
|
|
|
if entry, err := logical.StorageEntryJSON(entryIndex, &secretIDAccessorStorageEntry{
|
|
SecretIDHMAC: secretIDHMAC,
|
|
}); err != nil {
|
|
return err
|
|
} else if err = s.Put(ctx, entry); err != nil {
|
|
return fmt.Errorf("failed to persist accessor index entry: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deleteSecretIDAccessorEntry deletes the storage index mapping the accessor to a SecretID.
|
|
func (b *backend) deleteSecretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) error {
|
|
salt, err := b.Salt(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accessorPrefix := secretIDAccessorPrefix
|
|
if roleSecretIDPrefix == secretIDLocalPrefix {
|
|
accessorPrefix = secretIDAccessorLocalPrefix
|
|
}
|
|
entryIndex := accessorPrefix + salt.SaltID(secretIDAccessor)
|
|
|
|
accessorLock := b.secretIDAccessorLock(secretIDAccessor)
|
|
accessorLock.Lock()
|
|
defer accessorLock.Unlock()
|
|
|
|
// Delete the accessor of the SecretID first
|
|
if err := s.Delete(ctx, entryIndex); err != nil {
|
|
return fmt.Errorf("failed to delete accessor storage entry: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// flushRoleSecrets deletes all the SecretIDs that belong to the given
|
|
// RoleID.
|
|
func (b *backend) flushRoleSecrets(ctx context.Context, s logical.Storage, roleName, hmacKey, roleSecretIDPrefix string) error {
|
|
roleNameHMAC, err := createHMAC(hmacKey, roleName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create HMAC of role_name: %w", err)
|
|
}
|
|
|
|
// Acquire the custom lock to perform listing of SecretIDs
|
|
b.secretIDListingLock.RLock()
|
|
defer b.secretIDListingLock.RUnlock()
|
|
|
|
secretIDHMACs, err := s.List(ctx, fmt.Sprintf("%s%s/", roleSecretIDPrefix, roleNameHMAC))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, secretIDHMAC := range secretIDHMACs {
|
|
// Acquire the lock belonging to the SecretID
|
|
lock := b.secretIDLock(secretIDHMAC)
|
|
lock.Lock()
|
|
entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
|
|
if err := s.Delete(ctx, entryIndex); err != nil {
|
|
lock.Unlock()
|
|
return fmt.Errorf("error deleting SecretID %q from storage: %w", secretIDHMAC, err)
|
|
}
|
|
lock.Unlock()
|
|
}
|
|
return nil
|
|
}
|