package approle import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "strings" "sync" "time" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/cidrutil" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) // 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" structs:"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" structs:"secret_id_num_uses" mapstructure:"secret_id_num_uses"` // Duration after which this SecretID should expire. This is croleed by // the backend mount's max TTL value. SecretIDTTL time.Duration `json:"secret_id_ttl" structs:"secret_id_ttl" mapstructure:"secret_id_ttl"` // The time when the SecretID was created CreationTime time.Time `json:"creation_time" structs:"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" structs:"expiration_time" mapstructure:"expiration_time"` // The time representing the last time this storage entry was modified LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` // Metadata that belongs to the SecretID Metadata map[string]string `json:"metadata" structs:"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" structs:"cidr_list" mapstructure:"cidr_list"` } // 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" structs:"secret_id_hmac" mapstructure:"secret_id_hmac"` } // Checks if the Role represented by the RoleID still exists func (b *backend) validateRoleID(s logical.Storage, roleID string) (*roleStorageEntry, string, error) { // Look for the storage entry that maps the roleID to role roleIDIndex, err := b.roleIDEntry(s, roleID) if err != nil { return nil, "", err } if roleIDIndex == nil { return nil, "", fmt.Errorf("failed to find secondary index for role_id:%s\n", roleID) } role, err := b.roleEntry(s, roleIDIndex.Name) if err != nil { return nil, "", err } if role == nil { return nil, "", fmt.Errorf("role %s referred by the SecretID does not exist", roleIDIndex.Name) } return role, roleIDIndex.Name, nil } // Validates the supplied RoleID and SecretID func (b *backend) validateCredentials(req *logical.Request, data *framework.FieldData, sourceIP string) (*roleStorageEntry, string, map[string]string, error) { var metadata map[string]string // RoleID must be supplied during every login roleID := strings.TrimSpace(data.Get("role_id").(string)) if roleID == "" { return nil, "", metadata, fmt.Errorf("missing role_id") } // Validate the RoleID and get the Role entry role, roleName, err := b.validateRoleID(req.Storage, roleID) if err != nil { return nil, "", metadata, err } if role == nil || roleName == "" { return nil, "", metadata, fmt.Errorf("failed to validate role_id") } // Calculate the TTL boundaries since this reflects the properties of the token issued if role.TokenTTL, role.TokenMaxTTL, err = b.SanitizeTTL(role.TokenTTL, role.TokenMaxTTL); err != nil { return nil, "", metadata, err } if role.BindSecretID { // If 'bind_secret_id' was set on role, look for the field 'secret_id' // to be specified and validate it. secretID := strings.TrimSpace(data.Get("secret_id").(string)) if secretID == "" { return nil, "", metadata, fmt.Errorf("missing secret_id") } // Check if the SecretID supplied is valid. If use limit was specified // on the SecretID, it will be decremented in this call. var valid bool valid, metadata, err = b.validateBindSecretID(req.Storage, roleName, secretID, role.HMACKey, role.BoundCIDRList, sourceIP) if err != nil { return nil, "", metadata, err } if !valid { return nil, "", metadata, fmt.Errorf("invalid secret_id: %s\n", secretID) } } if role.BoundCIDRList != "" { // If 'bound_cidr_list' was set, verify the CIDR restrictions belongs, err := cidrutil.IPBelongsToCIDRBlocksString(sourceIP, role.BoundCIDRList, ",") if err != nil { return nil, "", metadata, fmt.Errorf("failed to verify the CIDR restrictions set on the role: %v", err) } if !belongs { return nil, "", metadata, fmt.Errorf("source address unauthorized through CIDR restrictions on the role") } } return role, roleName, metadata, nil } // validateBindSecretID is used to determine if the given SecretID is a valid one. func (b *backend) validateBindSecretID(s logical.Storage, roleName, secretID, hmacKey, roleBoundCIDRList, sourceIP string) (bool, map[string]string, error) { secretIDHMAC, err := createHMAC(hmacKey, secretID) if err != nil { return false, nil, fmt.Errorf("failed to create HMAC of secret_id: %s", err) } roleNameHMAC, err := createHMAC(hmacKey, roleName) if err != nil { return false, nil, fmt.Errorf("failed to create HMAC of role_name: %s", err) } entryIndex := fmt.Sprintf("secret_id/%s/%s", roleNameHMAC, secretIDHMAC) // SecretID locks are always index based on secretIDHMACs. This helps // acquiring the locks when the SecretIDs are listed. This allows grabbing // the correct locks even if the SecretIDs are not known in plaintext. lock := b.secretIDLock(secretIDHMAC) lock.RLock() result := secretIDStorageEntry{} if entry, err := s.Get(entryIndex); err != nil { lock.RUnlock() return false, nil, err } else if entry == nil { lock.RUnlock() return false, nil, nil } else if err := entry.DecodeJSON(&result); err != nil { lock.RUnlock() return false, nil, err } // SecretIDNumUses will be zero only if the usage limit was not set at all, // in which case, the SecretID will remain to be valid as long as it is not // expired. if result.SecretIDNumUses == 0 { // Ensure that the CIDRs on the secret ID are still a subset of that of // role's if err := verifyCIDRRoleSecretIDSubset(result.CIDRList, roleBoundCIDRList); err != nil { return false, nil, err } // If CIDR restrictions are present on the secret ID, check if the // source IP complies to it if len(result.CIDRList) != 0 { if belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(sourceIP, result.CIDRList); !belongs || err != nil { return false, nil, fmt.Errorf("source address unauthorized through CIDR restrictions on the secret ID: %v", err) } } lock.RUnlock() return true, result.Metadata, nil } // If the SecretIDNumUses is non-zero, it means that its use-count should be updated // in the storage. Switch the lock from a `read` to a `write` and update // the storage entry. lock.RUnlock() lock.Lock() defer lock.Unlock() // Lock switching may change the data. Refresh the contents. result = secretIDStorageEntry{} if entry, err := s.Get(entryIndex); err != nil { return false, nil, err } else if entry == nil { return false, nil, nil } else if err := entry.DecodeJSON(&result); err != nil { return false, nil, err } // If there exists a single use left, delete the SecretID entry from // the storage but do not fail the validation request. Subsequest // requests to use the same SecretID will fail. if result.SecretIDNumUses == 1 { // Delete the secret IDs accessor first if err := b.deleteSecretIDAccessorEntry(s, result.SecretIDAccessor); err != nil { return false, nil, err } if err := s.Delete(entryIndex); err != nil { return false, nil, fmt.Errorf("failed to delete SecretID: %s", err) } } else { // If the use count is greater than one, decrement it and update the last updated time. result.SecretIDNumUses -= 1 result.LastUpdatedTime = time.Now() if entry, err := logical.StorageEntryJSON(entryIndex, &result); err != nil { return false, nil, fmt.Errorf("failed to decrement the use count for SecretID:%s", secretID) } else if err = s.Put(entry); err != nil { return false, nil, fmt.Errorf("failed to decrement the use count for SecretID:%s", secretID) } } // Ensure that the CIDRs on the secret ID are still a subset of that of // role's if err := verifyCIDRRoleSecretIDSubset(result.CIDRList, roleBoundCIDRList); err != nil { return false, nil, err } // If CIDR restrictions are present on the secret ID, check if the // source IP complies to it if len(result.CIDRList) != 0 { if belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(sourceIP, result.CIDRList); !belongs || err != nil { return false, nil, fmt.Errorf("source address unauthorized through CIDR restrictions on the secret ID: %v", err) } } return true, result.Metadata, nil } // 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 { // Parse the CIDRs on role as a slice roleCIDRs := strutil.ParseDedupAndSortStrings(roleBoundCIDRList, ",") // If there are no CIDR blocks on the role, then the subset // requirement would be satisfied if len(roleCIDRs) != 0 { subset, err := cidrutil.SubsetBlocks(roleCIDRs, 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: %v", roleCIDRs, secretIDCIDRs, err) } } } return nil } // 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") } hm := hmac.New(sha256.New, []byte(key)) hm.Write([]byte(value)) return hex.EncodeToString(hm.Sum(nil)), nil } // secretIDLock is used to get a lock from the pre-initialized map of locks. // Map is indexed based on the first 2 characters of the secretIDHMAC. If the // input is not hex encoded or if empty, a "custom" lock will be returned. func (b *backend) secretIDLock(secretIDHMAC string) *sync.RWMutex { var lock *sync.RWMutex var ok bool if len(secretIDHMAC) >= 2 { lock, ok = b.secretIDLocksMap[secretIDHMAC[0:2]] } if !ok || lock == nil { // Fall back for custom lock to make sure that this method // never returns nil lock = b.secretIDLocksMap["custom"] } return lock } // secretIDAccessorLock is used to get a lock from the pre-initialized map // of locks. Map is indexed based on the first 2 characters of the // secretIDAccessor. If the input is not hex encoded or if empty, a "custom" // lock will be returned. func (b *backend) secretIDAccessorLock(secretIDAccessor string) *sync.RWMutex { var lock *sync.RWMutex var ok bool if len(secretIDAccessor) >= 2 { lock, ok = b.secretIDAccessorLocksMap[secretIDAccessor[0:2]] } if !ok || lock == nil { // Fall back for custom lock to make sure that this method // never returns nil lock = b.secretIDAccessorLocksMap["custom"] } return lock } // registerSecretIDEntry creates a new storage entry for the given SecretID. func (b *backend) registerSecretIDEntry(s logical.Storage, roleName, secretID, hmacKey string, secretEntry *secretIDStorageEntry) (*secretIDStorageEntry, error) { secretIDHMAC, err := createHMAC(hmacKey, secretID) if err != nil { return nil, fmt.Errorf("failed to create HMAC of secret_id: %s", err) } roleNameHMAC, err := createHMAC(hmacKey, roleName) if err != nil { return nil, fmt.Errorf("failed to create HMAC of role_name: %s", err) } entryIndex := fmt.Sprintf("secret_id/%s/%s", roleNameHMAC, secretIDHMAC) lock := b.secretIDLock(secretIDHMAC) lock.RLock() entry, err := s.Get(entryIndex) 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 = s.Get(entryIndex) 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 SecretIDTTL is not specified or if it crosses the backend mount's limit, // cap the expiration to backend's max. Otherwise, use it to determine the // expiration time. if secretEntry.SecretIDTTL < time.Duration(0) || secretEntry.SecretIDTTL > b.System().MaxLeaseTTL() { secretEntry.ExpirationTime = currentTime.Add(b.System().MaxLeaseTTL()) } else if secretEntry.SecretIDTTL != time.Duration(0) { // Set the ExpirationTime only if SecretIDTTL was set. SecretIDs should not // expire by default. secretEntry.ExpirationTime = currentTime.Add(secretEntry.SecretIDTTL) } // Before storing the SecretID, store its accessor. if err := b.createSecretIDAccessorEntry(s, secretEntry, secretIDHMAC); err != nil { return nil, err } if entry, err := logical.StorageEntryJSON(entryIndex, secretEntry); err != nil { return nil, err } else if err = s.Put(entry); err != nil { return nil, err } return secretEntry, nil } // secretIDAccessorEntry is used to read the storage entry that maps an // accessor to a secret_id. func (b *backend) secretIDAccessorEntry(s logical.Storage, secretIDAccessor string) (*secretIDAccessorStorageEntry, error) { if secretIDAccessor == "" { return nil, fmt.Errorf("missing secretIDAccessor") } var result secretIDAccessorStorageEntry // Create index entry, mapping the accessor to the token ID entryIndex := "accessor/" + b.salt.SaltID(secretIDAccessor) accessorLock := b.secretIDAccessorLock(secretIDAccessor) accessorLock.RLock() defer accessorLock.RUnlock() if entry, err := s.Get(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(s logical.Storage, entry *secretIDStorageEntry, secretIDHMAC 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 entryIndex := "accessor/" + b.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(entry); err != nil { return fmt.Errorf("failed to persist accessor index entry: %s", err) } return nil } // deleteSecretIDAccessorEntry deletes the storage index mapping the accessor to a SecretID. func (b *backend) deleteSecretIDAccessorEntry(s logical.Storage, secretIDAccessor string) error { accessorEntryIndex := "accessor/" + b.salt.SaltID(secretIDAccessor) accessorLock := b.secretIDAccessorLock(secretIDAccessor) accessorLock.Lock() defer accessorLock.Unlock() // Delete the accessor of the SecretID first if err := s.Delete(accessorEntryIndex); err != nil { return fmt.Errorf("failed to delete accessor storage entry: %s", err) } return nil } // flushRoleSecrets deletes all the SecretIDs that belong to the given // RoleID. func (b *backend) flushRoleSecrets(s logical.Storage, roleName, hmacKey string) error { roleNameHMAC, err := createHMAC(hmacKey, roleName) if err != nil { return fmt.Errorf("failed to create HMAC of role_name: %s", err) } // Acquire the custom lock to perform listing of SecretIDs customLock := b.secretIDLock("") customLock.RLock() defer customLock.RUnlock() secretIDHMACs, err := s.List(fmt.Sprintf("secret_id/%s/", 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("secret_id/%s/%s", roleNameHMAC, secretIDHMAC) if err := s.Delete(entryIndex); err != nil { lock.Unlock() return fmt.Errorf("error deleting SecretID %s from storage: %s", secretIDHMAC, err) } lock.Unlock() } return nil }