mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-17 03:57: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>
437 lines
13 KiB
Go
437 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cacheboltdb
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/hashicorp/go-hclog"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
|
"github.com/hashicorp/go-multierror"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
const (
|
|
// Keep track of schema version for future migrations
|
|
storageVersionKey = "version"
|
|
storageVersion = "2" // v2 merges auth-lease and secret-lease buckets into one ordered bucket
|
|
|
|
// DatabaseFileName - filename for the persistent cache file
|
|
DatabaseFileName = "vault-agent-cache.db"
|
|
|
|
// metaBucketName - naming the meta bucket that holds the version and
|
|
// bootstrapping keys
|
|
metaBucketName = "meta"
|
|
|
|
// DEPRECATED: secretLeaseType - v1 Bucket/type for leases with secret info
|
|
secretLeaseType = "secret-lease"
|
|
|
|
// DEPRECATED: authLeaseType - v1 Bucket/type for leases with auth info
|
|
authLeaseType = "auth-lease"
|
|
|
|
// TokenType - Bucket/type for auto-auth tokens
|
|
TokenType = "token"
|
|
|
|
// LeaseType - v2 Bucket/type for auth AND secret leases.
|
|
//
|
|
// This bucket stores keys in the same order they were created using
|
|
// auto-incrementing keys and the fact that BoltDB stores keys in byte
|
|
// slice order. This means when we iterate through this bucket during
|
|
// restore, we will always restore parent tokens before their children,
|
|
// allowing us to correctly attach child contexts to their parent's context.
|
|
LeaseType = "lease"
|
|
|
|
// lookupType - v2 Bucket/type to map from a memcachedb index ID to an
|
|
// auto-incrementing BoltDB key. Facilitates deletes from the lease
|
|
// bucket using an ID instead of the auto-incrementing BoltDB key.
|
|
lookupType = "lookup"
|
|
|
|
// AutoAuthToken - key for the latest auto-auth token
|
|
AutoAuthToken = "auto-auth-token"
|
|
|
|
// RetrievalTokenMaterial is the actual key or token in the key bucket
|
|
RetrievalTokenMaterial = "retrieval-token-material"
|
|
)
|
|
|
|
// BoltStorage is a persistent cache using a bolt db. Items are organized with
|
|
// the version and bootstrapping items in the "meta" bucket, and tokens, auth
|
|
// leases, and secret leases in their own buckets.
|
|
type BoltStorage struct {
|
|
db *bolt.DB
|
|
logger hclog.Logger
|
|
wrapper wrapping.Wrapper
|
|
aad string
|
|
}
|
|
|
|
// BoltStorageConfig is the collection of input parameters for setting up bolt
|
|
// storage
|
|
type BoltStorageConfig struct {
|
|
Path string
|
|
Logger hclog.Logger
|
|
Wrapper wrapping.Wrapper
|
|
AAD string
|
|
}
|
|
|
|
// NewBoltStorage opens a new bolt db at the specified file path and returns it.
|
|
// If the db already exists the buckets will just be created if they don't
|
|
// exist.
|
|
func NewBoltStorage(config *BoltStorageConfig) (*BoltStorage, error) {
|
|
dbPath := filepath.Join(config.Path, DatabaseFileName)
|
|
db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
return createBoltSchema(tx, storageVersion)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bs := &BoltStorage{
|
|
db: db,
|
|
logger: config.Logger,
|
|
wrapper: config.Wrapper,
|
|
aad: config.AAD,
|
|
}
|
|
return bs, nil
|
|
}
|
|
|
|
func createBoltSchema(tx *bolt.Tx, createVersion string) error {
|
|
switch {
|
|
case createVersion == "1":
|
|
if err := createV1BoltSchema(tx); err != nil {
|
|
return err
|
|
}
|
|
case createVersion == "2":
|
|
if err := createV2BoltSchema(tx); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("schema version %s not supported", createVersion)
|
|
}
|
|
|
|
meta, err := tx.CreateBucketIfNotExists([]byte(metaBucketName))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create bucket %s: %w", metaBucketName, err)
|
|
}
|
|
|
|
// Check and set file version in the meta bucket.
|
|
version := meta.Get([]byte(storageVersionKey))
|
|
switch {
|
|
case version == nil:
|
|
err = meta.Put([]byte(storageVersionKey), []byte(createVersion))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set storage version: %w", err)
|
|
}
|
|
|
|
return nil
|
|
|
|
case string(version) == createVersion:
|
|
return nil
|
|
|
|
case string(version) == "1" && createVersion == "2":
|
|
return migrateFromV1ToV2Schema(tx)
|
|
|
|
default:
|
|
return fmt.Errorf("storage migration from %s to %s not implemented", string(version), createVersion)
|
|
}
|
|
}
|
|
|
|
func createV1BoltSchema(tx *bolt.Tx) error {
|
|
// Create the buckets for tokens and leases.
|
|
for _, bucket := range []string{TokenType, authLeaseType, secretLeaseType} {
|
|
if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil {
|
|
return fmt.Errorf("failed to create %s bucket: %w", bucket, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createV2BoltSchema(tx *bolt.Tx) error {
|
|
// Create the buckets for tokens and leases.
|
|
for _, bucket := range []string{TokenType, LeaseType, lookupType} {
|
|
if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil {
|
|
return fmt.Errorf("failed to create %s bucket: %w", bucket, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func migrateFromV1ToV2Schema(tx *bolt.Tx) error {
|
|
if err := createV2BoltSchema(tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v1BucketType := range []string{authLeaseType, secretLeaseType} {
|
|
if bucket := tx.Bucket([]byte(v1BucketType)); bucket != nil {
|
|
bucket.ForEach(func(key, value []byte) error {
|
|
autoIncKey, err := autoIncrementedLeaseKey(tx, string(key))
|
|
if err != nil {
|
|
return fmt.Errorf("error migrating %s %q key to auto incremented key: %w", v1BucketType, string(key), err)
|
|
}
|
|
if err := tx.Bucket([]byte(LeaseType)).Put(autoIncKey, value); err != nil {
|
|
return fmt.Errorf("error migrating %s %q from v1 to v2 schema: %w", v1BucketType, string(key), err)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err := tx.DeleteBucket([]byte(v1BucketType)); err != nil {
|
|
return fmt.Errorf("failed to clean up %s bucket during v1 to v2 schema migration: %w", v1BucketType, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
meta, err := tx.CreateBucketIfNotExists([]byte(metaBucketName))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create meta bucket: %w", err)
|
|
}
|
|
if err := meta.Put([]byte(storageVersionKey), []byte(storageVersion)); err != nil {
|
|
return fmt.Errorf("failed to update schema from v1 to v2: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func autoIncrementedLeaseKey(tx *bolt.Tx, id string) ([]byte, error) {
|
|
leaseBucket := tx.Bucket([]byte(LeaseType))
|
|
keyValue, err := leaseBucket.NextSequence()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate lookup key for id %q: %w", id, err)
|
|
}
|
|
|
|
key := make([]byte, 8)
|
|
// MUST be big endian, because keys are ordered by byte slice comparison
|
|
// which progressively compares each byte in the slice starting at index 0.
|
|
// BigEndian in the range [255-257] looks like this:
|
|
// [0 0 0 0 0 0 0 255]
|
|
// [0 0 0 0 0 0 1 0]
|
|
// [0 0 0 0 0 0 1 1]
|
|
// LittleEndian in the same range looks like this:
|
|
// [255 0 0 0 0 0 0 0]
|
|
// [0 1 0 0 0 0 0 0]
|
|
// [1 1 0 0 0 0 0 0]
|
|
binary.BigEndian.PutUint64(key, keyValue)
|
|
|
|
err = tx.Bucket([]byte(lookupType)).Put([]byte(id), key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// Set an index (token or lease) in bolt storage
|
|
func (b *BoltStorage) Set(ctx context.Context, id string, plaintext []byte, indexType string) error {
|
|
blob, err := b.wrapper.Encrypt(ctx, plaintext, wrapping.WithAad([]byte(b.aad)))
|
|
if err != nil {
|
|
return fmt.Errorf("error encrypting %s index: %w", indexType, err)
|
|
}
|
|
|
|
protoBlob, err := proto.Marshal(blob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.db.Update(func(tx *bolt.Tx) error {
|
|
var key []byte
|
|
switch indexType {
|
|
case LeaseType:
|
|
// If this is a lease type, generate an auto-incrementing key and
|
|
// store an ID -> key lookup entry
|
|
key, err = autoIncrementedLeaseKey(tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case TokenType:
|
|
// If this is an auto-auth token, also stash it in the meta bucket for
|
|
// easy retrieval upon restore
|
|
key = []byte(id)
|
|
meta := tx.Bucket([]byte(metaBucketName))
|
|
if err := meta.Put([]byte(AutoAuthToken), protoBlob); err != nil {
|
|
return fmt.Errorf("failed to set latest auto-auth token: %w", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("called Set for unsupported type %q", indexType)
|
|
}
|
|
s := tx.Bucket([]byte(indexType))
|
|
if s == nil {
|
|
return fmt.Errorf("bucket %q not found", indexType)
|
|
}
|
|
return s.Put(key, protoBlob)
|
|
})
|
|
}
|
|
|
|
// Delete an index (token or lease) by key from bolt storage
|
|
func (b *BoltStorage) Delete(id string, indexType string) error {
|
|
return b.db.Update(func(tx *bolt.Tx) error {
|
|
key := []byte(id)
|
|
if indexType == LeaseType {
|
|
key = tx.Bucket([]byte(lookupType)).Get(key)
|
|
if key == nil {
|
|
return fmt.Errorf("failed to lookup bolt DB key for id %q", id)
|
|
}
|
|
|
|
err := tx.Bucket([]byte(lookupType)).Delete([]byte(id))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete %q from lookup bucket: %w", id, err)
|
|
}
|
|
}
|
|
|
|
bucket := tx.Bucket([]byte(indexType))
|
|
if bucket == nil {
|
|
return fmt.Errorf("bucket %q not found during delete", indexType)
|
|
}
|
|
if err := bucket.Delete(key); err != nil {
|
|
return fmt.Errorf("failed to delete %q from %q bucket: %w", id, indexType, err)
|
|
}
|
|
b.logger.Trace("deleted index from bolt db", "id", id)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (b *BoltStorage) decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
|
|
var blob wrapping.BlobInfo
|
|
if err := proto.Unmarshal(ciphertext, &blob); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b.wrapper.Decrypt(ctx, &blob, wrapping.WithAad([]byte(b.aad)))
|
|
}
|
|
|
|
// GetByType returns a list of stored items of the specified type
|
|
func (b *BoltStorage) GetByType(ctx context.Context, indexType string) ([][]byte, error) {
|
|
var returnBytes [][]byte
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
var errors *multierror.Error
|
|
|
|
bucket := tx.Bucket([]byte(indexType))
|
|
if bucket == nil {
|
|
return fmt.Errorf("bucket %q not found", indexType)
|
|
}
|
|
bucket.ForEach(func(key, ciphertext []byte) error {
|
|
plaintext, err := b.decrypt(ctx, ciphertext)
|
|
if err != nil {
|
|
errors = multierror.Append(errors, fmt.Errorf("error decrypting entry %s: %w", key, err))
|
|
return nil
|
|
}
|
|
|
|
returnBytes = append(returnBytes, plaintext)
|
|
return nil
|
|
})
|
|
return errors.ErrorOrNil()
|
|
})
|
|
|
|
return returnBytes, err
|
|
}
|
|
|
|
// GetAutoAuthToken retrieves the latest auto-auth token, and returns nil if non
|
|
// exists yet
|
|
func (b *BoltStorage) GetAutoAuthToken(ctx context.Context) ([]byte, error) {
|
|
var encryptedToken []byte
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
meta := tx.Bucket([]byte(metaBucketName))
|
|
if meta == nil {
|
|
return fmt.Errorf("bucket %q not found", metaBucketName)
|
|
}
|
|
value := meta.Get([]byte(AutoAuthToken))
|
|
if value != nil {
|
|
encryptedToken = make([]byte, len(value))
|
|
copy(encryptedToken, value)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if encryptedToken == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
plaintext, err := b.decrypt(ctx, encryptedToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt auto-auth token: %w", err)
|
|
}
|
|
return plaintext, nil
|
|
}
|
|
|
|
// GetRetrievalToken retrieves a plaintext token from the KeyBucket, which will
|
|
// be used by the key manager to retrieve the encryption key, nil if none set
|
|
func (b *BoltStorage) GetRetrievalToken() ([]byte, error) {
|
|
var token []byte
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
metaBucket := tx.Bucket([]byte(metaBucketName))
|
|
if metaBucket == nil {
|
|
return fmt.Errorf("bucket %q not found", metaBucketName)
|
|
}
|
|
value := metaBucket.Get([]byte(RetrievalTokenMaterial))
|
|
if value != nil {
|
|
token = make([]byte, len(value))
|
|
copy(token, value)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return token, err
|
|
}
|
|
|
|
// StoreRetrievalToken sets plaintext token material in the RetrievalTokenBucket
|
|
func (b *BoltStorage) StoreRetrievalToken(token []byte) error {
|
|
return b.db.Update(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket([]byte(metaBucketName))
|
|
if bucket == nil {
|
|
return fmt.Errorf("bucket %q not found", metaBucketName)
|
|
}
|
|
return bucket.Put([]byte(RetrievalTokenMaterial), token)
|
|
})
|
|
}
|
|
|
|
// Close the boltdb
|
|
func (b *BoltStorage) Close() error {
|
|
b.logger.Trace("closing bolt db", "path", b.db.Path())
|
|
return b.db.Close()
|
|
}
|
|
|
|
// Clear the boltdb by deleting all the token and lease buckets and recreating
|
|
// the schema/layout
|
|
func (b *BoltStorage) Clear() error {
|
|
return b.db.Update(func(tx *bolt.Tx) error {
|
|
for _, name := range []string{TokenType, LeaseType, lookupType} {
|
|
b.logger.Trace("deleting bolt bucket", "name", name)
|
|
if err := tx.DeleteBucket([]byte(name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return createBoltSchema(tx, storageVersion)
|
|
})
|
|
}
|
|
|
|
// DBFileExists checks whether the vault agent cache file at `filePath` exists
|
|
func DBFileExists(path string) (bool, error) {
|
|
checkFile, err := os.OpenFile(filepath.Join(path, DatabaseFileName), os.O_RDWR, 0o600)
|
|
defer checkFile.Close()
|
|
switch {
|
|
case err == nil:
|
|
return true, nil
|
|
case os.IsNotExist(err):
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("failed to check if bolt file exists at path %s: %w", path, err)
|
|
}
|
|
}
|