342b61984a
Move version out of SDK. For now it's a copy rather than move: the part not addressed by this change is sdk/helper/useragent.String, which we'll want to remove in favour of PluginString. That will have to wait until we've removed uses of useragent.String from all builtins.
3554 lines
109 KiB
Go
3554 lines
109 KiB
Go
package vault
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/armon/go-metrics"
|
|
"github.com/hashicorp/errwrap"
|
|
log "github.com/hashicorp/go-hclog"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
|
aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2"
|
|
"github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/go-secure-stdlib/mlock"
|
|
"github.com/hashicorp/go-secure-stdlib/reloadutil"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/go-secure-stdlib/tlsutil"
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/audit"
|
|
"github.com/hashicorp/vault/command/server"
|
|
"github.com/hashicorp/vault/helper/identity/mfa"
|
|
"github.com/hashicorp/vault/helper/locking"
|
|
"github.com/hashicorp/vault/helper/metricsutil"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/helper/osutil"
|
|
"github.com/hashicorp/vault/physical/raft"
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
"github.com/hashicorp/vault/sdk/helper/logging"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/hashicorp/vault/sdk/physical"
|
|
sr "github.com/hashicorp/vault/serviceregistration"
|
|
"github.com/hashicorp/vault/shamir"
|
|
"github.com/hashicorp/vault/vault/cluster"
|
|
"github.com/hashicorp/vault/vault/quotas"
|
|
vaultseal "github.com/hashicorp/vault/vault/seal"
|
|
"github.com/hashicorp/vault/version"
|
|
"github.com/patrickmn/go-cache"
|
|
uberAtomic "go.uber.org/atomic"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
const (
|
|
// CoreLockPath is the path used to acquire a coordinating lock
|
|
// for a highly-available deploy.
|
|
CoreLockPath = "core/lock"
|
|
|
|
// The poison pill is used as a check during certain scenarios to indicate
|
|
// to standby nodes that they should seal
|
|
poisonPillPath = "core/poison-pill"
|
|
poisonPillDRPath = "core/poison-pill-dr"
|
|
|
|
// coreLeaderPrefix is the prefix used for the UUID that contains
|
|
// the currently elected leader.
|
|
coreLeaderPrefix = "core/leader/"
|
|
|
|
// coreKeyringCanaryPath is used as a canary to indicate to replicated
|
|
// clusters that they need to perform a rekey operation synchronously; this
|
|
// isn't keyring-canary to avoid ignoring it when ignoring core/keyring
|
|
coreKeyringCanaryPath = "core/canary-keyring"
|
|
|
|
indexHeaderHMACKeyPath = "core/index-header-hmac-key"
|
|
|
|
// defaultMFAAuthResponseTTL is the default duration that Vault caches the
|
|
// MfaAuthResponse when the value is not specified in the server config
|
|
defaultMFAAuthResponseTTL = 300 * time.Second
|
|
|
|
// defaultMaxTOTPValidateAttempts is the default value for the number
|
|
// of failed attempts to validate a request subject to TOTP MFA. If the
|
|
// number of failed totp passcode validations exceeds this max value, the
|
|
// user needs to wait until a fresh totp passcode is generated.
|
|
defaultMaxTOTPValidateAttempts = 5
|
|
|
|
// ForwardSSCTokenToActive is the value that must be set in the
|
|
// forwardToActive to trigger forwarding if a perf standby encounters
|
|
// an SSC Token that it does not have the WAL state for.
|
|
ForwardSSCTokenToActive = "new_token"
|
|
|
|
WrapperTypeHsmAutoDeprecated = wrapping.WrapperType("hsm-auto")
|
|
|
|
// undoLogsAreSafeStoragePath is a storage path that we write once we know undo logs are
|
|
// safe, so we don't have to keep checking all the time.
|
|
undoLogsAreSafeStoragePath = "core/raft/undo_logs_are_safe"
|
|
)
|
|
|
|
var (
|
|
// ErrAlreadyInit is returned if the core is already
|
|
// initialized. This prevents a re-initialization.
|
|
ErrAlreadyInit = errors.New("Vault is already initialized")
|
|
|
|
// ErrNotInit is returned if a non-initialized barrier
|
|
// is attempted to be unsealed.
|
|
ErrNotInit = errors.New("Vault is not initialized")
|
|
|
|
// ErrInternalError is returned when we don't want to leak
|
|
// any information about an internal error
|
|
ErrInternalError = errors.New("internal error")
|
|
|
|
// ErrHANotEnabled is returned if the operation only makes sense
|
|
// in an HA setting
|
|
ErrHANotEnabled = errors.New("Vault is not configured for highly-available mode")
|
|
|
|
// manualStepDownSleepPeriod is how long to sleep after a user-initiated
|
|
// step down of the active node, to prevent instantly regrabbing the lock.
|
|
// It's var not const so that tests can manipulate it.
|
|
manualStepDownSleepPeriod = 10 * time.Second
|
|
|
|
// Functions only in the Enterprise version
|
|
enterprisePostUnseal = enterprisePostUnsealImpl
|
|
enterprisePreSeal = enterprisePreSealImpl
|
|
enterpriseSetupFilteredPaths = enterpriseSetupFilteredPathsImpl
|
|
enterpriseSetupQuotas = enterpriseSetupQuotasImpl
|
|
enterpriseSetupAPILock = setupAPILockImpl
|
|
startReplication = startReplicationImpl
|
|
stopReplication = stopReplicationImpl
|
|
LastWAL = lastWALImpl
|
|
LastPerformanceWAL = lastPerformanceWALImpl
|
|
LastDRWAL = lastDRWALImpl
|
|
PerformanceMerkleRoot = merkleRootImpl
|
|
DRMerkleRoot = merkleRootImpl
|
|
LastRemoteWAL = lastRemoteWALImpl
|
|
LastRemoteUpstreamWAL = lastRemoteUpstreamWALImpl
|
|
WaitUntilWALShipped = waitUntilWALShippedImpl
|
|
storedLicenseCheck = func(c *Core, conf *CoreConfig) error { return nil }
|
|
LicenseAutoloaded = func(*Core) bool { return false }
|
|
LicenseInitCheck = func(*Core) error { return nil }
|
|
LicenseSummary = func(*Core) (*LicenseState, error) { return nil, nil }
|
|
LicenseReload = func(*Core) error { return nil }
|
|
)
|
|
|
|
// NonFatalError is an error that can be returned during NewCore that should be
|
|
// displayed but not cause a program exit
|
|
type NonFatalError struct {
|
|
Err error
|
|
}
|
|
|
|
func (e *NonFatalError) WrappedErrors() []error {
|
|
return []error{e.Err}
|
|
}
|
|
|
|
func (e *NonFatalError) Error() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// NewNonFatalError returns a new non-fatal error.
|
|
func NewNonFatalError(err error) *NonFatalError {
|
|
return &NonFatalError{Err: err}
|
|
}
|
|
|
|
// IsFatalError returns true if the given error is a fatal error.
|
|
func IsFatalError(err error) bool {
|
|
return !errwrap.ContainsType(err, new(NonFatalError))
|
|
}
|
|
|
|
// ErrInvalidKey is returned if there is a user-based error with a provided
|
|
// unseal key. This will be shown to the user, so should not contain
|
|
// information that is sensitive.
|
|
type ErrInvalidKey struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *ErrInvalidKey) Error() string {
|
|
return fmt.Sprintf("invalid key: %v", e.Reason)
|
|
}
|
|
|
|
type RegisterAuthFunc func(context.Context, time.Duration, string, *logical.Auth, string) error
|
|
|
|
type activeAdvertisement struct {
|
|
RedirectAddr string `json:"redirect_addr"`
|
|
ClusterAddr string `json:"cluster_addr,omitempty"`
|
|
ClusterCert []byte `json:"cluster_cert,omitempty"`
|
|
ClusterKeyParams *certutil.ClusterKeyParams `json:"cluster_key_params,omitempty"`
|
|
}
|
|
|
|
type unlockInformation struct {
|
|
Parts [][]byte
|
|
Nonce string
|
|
}
|
|
|
|
type raftInformation struct {
|
|
challenge *wrapping.BlobInfo
|
|
leaderClient *api.Client
|
|
leaderBarrierConfig *SealConfig
|
|
nonVoter bool
|
|
joinInProgress bool
|
|
}
|
|
|
|
type migrationInformation struct {
|
|
// seal to use during a migration operation. It is the
|
|
// seal we're migrating *from*.
|
|
seal Seal
|
|
|
|
// unsealKey was the unseal key provided for the migration seal.
|
|
// This will be set as the recovery key when migrating from shamir to auto-seal.
|
|
// We don't need to do anything with it when migrating auto->shamir because
|
|
// we don't store the shamir combined key for shamir seals, nor when
|
|
// migrating auto->auto because then the recovery key doesn't change.
|
|
unsealKey []byte
|
|
}
|
|
|
|
// Core is used as the central manager of Vault activity. It is the primary point of
|
|
// interface for API handlers and is responsible for managing the logical and physical
|
|
// backends, router, security barrier, and audit trails.
|
|
type Core struct {
|
|
entCore
|
|
|
|
// The registry of builtin plugins is passed in here as an interface because
|
|
// if it's used directly, it results in import cycles.
|
|
builtinRegistry BuiltinRegistry
|
|
|
|
// N.B.: This is used to populate a dev token down replication, as
|
|
// otherwise, after replication is started, a dev would have to go through
|
|
// the generate-root process simply to talk to the new follower cluster.
|
|
devToken string
|
|
|
|
// HABackend may be available depending on the physical backend
|
|
ha physical.HABackend
|
|
|
|
// storageType is the the storage type set in the storage configuration
|
|
storageType string
|
|
|
|
// redirectAddr is the address we advertise as leader if held
|
|
redirectAddr string
|
|
|
|
// clusterAddr is the address we use for clustering
|
|
clusterAddr *atomic.Value
|
|
|
|
// physical backend is the un-trusted backend with durable data
|
|
physical physical.Backend
|
|
|
|
// serviceRegistration is the ServiceRegistration network
|
|
serviceRegistration sr.ServiceRegistration
|
|
|
|
// hcpLinkStatus is a string describing the status of HCP link connection
|
|
hcpLinkStatus HCPLinkStatus
|
|
|
|
// underlyingPhysical will always point to the underlying backend
|
|
// implementation. This is an un-trusted backend with durable data
|
|
underlyingPhysical physical.Backend
|
|
|
|
// seal is our seal, for seal configuration information
|
|
seal Seal
|
|
|
|
// raftJoinDoneCh is used by the raft retry join routine to inform unseal process
|
|
// that the join is complete
|
|
raftJoinDoneCh chan struct{}
|
|
|
|
// postUnsealStarted informs the raft retry join routine that unseal key
|
|
// validation is completed and post unseal has started so that it can complete
|
|
// the join process when Shamir seal is in use
|
|
postUnsealStarted *uint32
|
|
|
|
// raftInfo will contain information required for this node to join as a
|
|
// peer to an existing raft cluster. This is marked atomic to prevent data
|
|
// races and casted to raftInformation wherever it is used.
|
|
raftInfo *atomic.Value
|
|
|
|
// migrationInfo is used during (and possibly after) a seal migration.
|
|
// This contains information about the seal we are migrating *from*. Even
|
|
// post seal migration, provided the old seal is still in configuration
|
|
// migrationInfo will be populated, which on enterprise may be necessary for
|
|
// seal rewrap.
|
|
migrationInfo *migrationInformation
|
|
sealMigrationDone *uint32
|
|
|
|
// barrier is the security barrier wrapping the physical backend
|
|
barrier SecurityBarrier
|
|
|
|
// router is responsible for managing the mount points for logical backends.
|
|
router *Router
|
|
|
|
// logicalBackends is the mapping of backends to use for this core
|
|
logicalBackends map[string]logical.Factory
|
|
|
|
// credentialBackends is the mapping of backends to use for this core
|
|
credentialBackends map[string]logical.Factory
|
|
|
|
// auditBackends is the mapping of backends to use for this core
|
|
auditBackends map[string]audit.Factory
|
|
|
|
// stateLock protects mutable state
|
|
stateLock locking.DeadlockRWMutex
|
|
sealed *uint32
|
|
|
|
standby bool
|
|
perfStandby bool
|
|
standbyDoneCh chan struct{}
|
|
standbyStopCh *atomic.Value
|
|
manualStepDownCh chan struct{}
|
|
keepHALockOnStepDown *uint32
|
|
heldHALock physical.Lock
|
|
|
|
// shutdownDoneCh is used to notify when Shutdown() completes
|
|
shutdownDoneCh chan struct{}
|
|
|
|
// unlockInfo has the keys provided to Unseal until the threshold number of parts is available, as well as the operation nonce
|
|
unlockInfo *unlockInformation
|
|
|
|
// generateRootProgress holds the shares until we reach enough
|
|
// to verify the master key
|
|
generateRootConfig *GenerateRootConfig
|
|
generateRootProgress [][]byte
|
|
generateRootLock sync.Mutex
|
|
|
|
// These variables holds the config and shares we have until we reach
|
|
// enough to verify the appropriate master key. Note that the same lock is
|
|
// used; this isn't time-critical so this shouldn't be a problem.
|
|
barrierRekeyConfig *SealConfig
|
|
recoveryRekeyConfig *SealConfig
|
|
rekeyLock sync.RWMutex
|
|
|
|
// mounts is loaded after unseal since it is a protected
|
|
// configuration
|
|
mounts *MountTable
|
|
|
|
// mountsLock is used to ensure that the mounts table does not
|
|
// change underneath a calling function
|
|
mountsLock sync.RWMutex
|
|
|
|
// mountMigrationTracker tracks past and ongoing remount operations
|
|
// against their migration ids
|
|
mountMigrationTracker *sync.Map
|
|
|
|
// auth is loaded after unseal since it is a protected
|
|
// configuration
|
|
auth *MountTable
|
|
|
|
// authLock is used to ensure that the auth table does not
|
|
// change underneath a calling function
|
|
authLock sync.RWMutex
|
|
|
|
// audit is loaded after unseal since it is a protected
|
|
// configuration
|
|
audit *MountTable
|
|
|
|
// auditLock is used to ensure that the audit table does not
|
|
// change underneath a calling function
|
|
auditLock sync.RWMutex
|
|
|
|
// auditBroker is used to ingest the audit events and fan
|
|
// out into the configured audit backends
|
|
auditBroker *AuditBroker
|
|
|
|
// auditedHeaders is used to configure which http headers
|
|
// can be output in the audit logs
|
|
auditedHeaders *AuditedHeadersConfig
|
|
|
|
// systemBackend is the backend which is used to manage internal operations
|
|
systemBackend *SystemBackend
|
|
loginMFABackend *LoginMFABackend
|
|
|
|
// cubbyholeBackend is the backend which manages the per-token storage
|
|
cubbyholeBackend *CubbyholeBackend
|
|
|
|
// systemBarrierView is the barrier view for the system backend
|
|
systemBarrierView *BarrierView
|
|
|
|
// expiration manager is used for managing LeaseIDs,
|
|
// renewal, expiration and revocation
|
|
expiration *ExpirationManager
|
|
|
|
// rollback manager is used to run rollbacks periodically
|
|
rollback *RollbackManager
|
|
|
|
// policy store is used to manage named ACL policies
|
|
policyStore *PolicyStore
|
|
|
|
// token store is used to manage authentication tokens
|
|
tokenStore *TokenStore
|
|
|
|
// identityStore is used to manage client entities
|
|
identityStore *IdentityStore
|
|
|
|
// activityLog is used to track active client count
|
|
activityLog *ActivityLog
|
|
|
|
// metricsCh is used to stop the metrics streaming
|
|
metricsCh chan struct{}
|
|
|
|
// metricsMutex is used to prevent a race condition between
|
|
// metrics emission and sealing leading to a nil pointer
|
|
metricsMutex sync.Mutex
|
|
|
|
// inFlightReqMap is used to store info about in-flight requests
|
|
inFlightReqData *InFlightRequests
|
|
|
|
// mfaResponseAuthQueue is used to cache the auth response per request ID
|
|
mfaResponseAuthQueue *LoginMFAPriorityQueue
|
|
mfaResponseAuthQueueLock sync.Mutex
|
|
|
|
// metricSink is the destination for all metrics that have
|
|
// a cluster label.
|
|
metricSink *metricsutil.ClusterMetricSink
|
|
|
|
defaultLeaseTTL time.Duration
|
|
maxLeaseTTL time.Duration
|
|
|
|
// baseLogger is used to avoid ResetNamed as it strips useful prefixes in
|
|
// e.g. testing
|
|
baseLogger log.Logger
|
|
logger log.Logger
|
|
|
|
// log level provided by config, CLI flag, or env
|
|
logLevel string
|
|
|
|
// Disables the trace display for Sentinel checks
|
|
sentinelTraceDisabled bool
|
|
|
|
// cachingDisabled indicates whether caches are disabled
|
|
cachingDisabled bool
|
|
// Cache stores the actual cache; we always have this but may bypass it if
|
|
// disabled
|
|
physicalCache physical.ToggleablePurgemonster
|
|
|
|
// logRequestsLevel indicates at which level requests should be logged
|
|
logRequestsLevel *uberAtomic.Int32
|
|
|
|
// reloadFuncs is a map containing reload functions
|
|
reloadFuncs map[string][]reloadutil.ReloadFunc
|
|
|
|
// reloadFuncsLock controls access to the funcs
|
|
reloadFuncsLock sync.RWMutex
|
|
|
|
// wrappingJWTKey is the key used for generating JWTs containing response
|
|
// wrapping information
|
|
wrappingJWTKey *ecdsa.PrivateKey
|
|
|
|
//
|
|
// Cluster information
|
|
//
|
|
// Name
|
|
clusterName string
|
|
// ID
|
|
clusterID uberAtomic.String
|
|
// Specific cipher suites to use for clustering, if any
|
|
clusterCipherSuites []uint16
|
|
// Used to modify cluster parameters
|
|
clusterParamsLock sync.RWMutex
|
|
// The private key stored in the barrier used for establishing
|
|
// mutually-authenticated connections between Vault cluster members
|
|
localClusterPrivateKey *atomic.Value
|
|
// The local cluster cert
|
|
localClusterCert *atomic.Value
|
|
// The parsed form of the local cluster cert
|
|
localClusterParsedCert *atomic.Value
|
|
// The TCP addresses we should use for clustering
|
|
clusterListenerAddrs []*net.TCPAddr
|
|
// The handler to use for request forwarding
|
|
clusterHandler http.Handler
|
|
// Write lock used to ensure that we don't have multiple connections adjust
|
|
// this value at the same time
|
|
requestForwardingConnectionLock sync.RWMutex
|
|
// Lock for the leader values, ensuring we don't run the parts of Leader()
|
|
// that change things concurrently
|
|
leaderParamsLock sync.RWMutex
|
|
// Current cluster leader values
|
|
clusterLeaderParams *atomic.Value
|
|
// Info on cluster members
|
|
clusterPeerClusterAddrsCache *cache.Cache
|
|
// The context for the client
|
|
rpcClientConnContext context.Context
|
|
// The function for canceling the client connection
|
|
rpcClientConnCancelFunc context.CancelFunc
|
|
// The grpc ClientConn for RPC calls
|
|
rpcClientConn *grpc.ClientConn
|
|
// The grpc forwarding client
|
|
rpcForwardingClient *forwardingClient
|
|
// The UUID used to hold the leader lock. Only set on active node
|
|
leaderUUID string
|
|
|
|
// CORS Information
|
|
corsConfig *CORSConfig
|
|
|
|
// The active set of upstream cluster addresses; stored via the Echo
|
|
// mechanism, loaded by the balancer
|
|
atomicPrimaryClusterAddrs *atomic.Value
|
|
|
|
atomicPrimaryFailoverAddrs *atomic.Value
|
|
|
|
// replicationState keeps the current replication state cached for quick
|
|
// lookup; activeNodeReplicationState stores the active value on standbys
|
|
replicationState *uint32
|
|
activeNodeReplicationState *uint32
|
|
|
|
// uiConfig contains UI configuration
|
|
uiConfig *UIConfig
|
|
|
|
// rawEnabled indicates whether the Raw endpoint is enabled
|
|
rawEnabled bool
|
|
|
|
// pluginDirectory is the location vault will look for plugin binaries
|
|
pluginDirectory string
|
|
|
|
// pluginFileUid is the uid of the plugin files and directory
|
|
pluginFileUid int
|
|
|
|
// pluginFilePermissions is the permissions of the plugin files and directory
|
|
pluginFilePermissions int
|
|
|
|
// pluginCatalog is used to manage plugin configurations
|
|
pluginCatalog *PluginCatalog
|
|
|
|
// The userFailedLoginInfo map has user failed login information.
|
|
// It has user information (alias-name and mount accessor) as a key
|
|
// and login counter, last failed login time as value
|
|
userFailedLoginInfo map[FailedLoginUser]*FailedLoginInfo
|
|
|
|
// userFailedLoginInfoLock controls access to the userFailedLoginInfoMap
|
|
userFailedLoginInfoLock sync.RWMutex
|
|
|
|
enableMlock bool
|
|
|
|
// This can be used to trigger operations to stop running when Vault is
|
|
// going to be shut down, stepped down, or sealed
|
|
activeContext context.Context
|
|
activeContextCancelFunc *atomic.Value
|
|
|
|
// Stores the sealunwrapper for downgrade needs
|
|
sealUnwrapper physical.Backend
|
|
|
|
// unsealwithStoredKeysLock is a mutex that prevents multiple processes from
|
|
// unsealing with stored keys are the same time.
|
|
unsealWithStoredKeysLock sync.Mutex
|
|
|
|
// Stores any funcs that should be run on successful postUnseal
|
|
postUnsealFuncs []func()
|
|
|
|
// Stores any funcs that should be run on successful barrier unseal in
|
|
// recovery mode
|
|
postRecoveryUnsealFuncs []func() error
|
|
|
|
// replicationFailure is used to mark when replication has entered an
|
|
// unrecoverable failure.
|
|
replicationFailure *uint32
|
|
|
|
// disablePerfStanby is used to tell a standby not to attempt to become a
|
|
// perf standby
|
|
disablePerfStandby bool
|
|
|
|
licensingStopCh chan struct{}
|
|
|
|
// Stores loggers so we can reset the level
|
|
allLoggers []log.Logger
|
|
allLoggersLock sync.RWMutex
|
|
|
|
// Can be toggled atomically to cause the core to never try to become
|
|
// active, or give up active as soon as it gets it
|
|
neverBecomeActive *uint32
|
|
|
|
// loadCaseSensitiveIdentityStore enforces the loading of identity store
|
|
// artifacts in a case sensitive manner. To be used only in testing.
|
|
loadCaseSensitiveIdentityStore bool
|
|
|
|
// clusterListener starts up and manages connections on the cluster ports
|
|
clusterListener *atomic.Value
|
|
|
|
// customListenerHeader holds custom response headers for a listener
|
|
customListenerHeader *atomic.Value
|
|
|
|
// Telemetry objects
|
|
metricsHelper *metricsutil.MetricsHelper
|
|
|
|
// raftFollowerStates tracks information about all the raft follower nodes.
|
|
raftFollowerStates *raft.FollowerStates
|
|
// Stop channel for raft TLS rotations
|
|
raftTLSRotationStopCh chan struct{}
|
|
// Stores the pending peers we are waiting to give answers
|
|
pendingRaftPeers *sync.Map
|
|
|
|
// rawConfig stores the config as-is from the provided server configuration.
|
|
rawConfig *atomic.Value
|
|
|
|
coreNumber int
|
|
|
|
// secureRandomReader is the reader used for CSP operations
|
|
secureRandomReader io.Reader
|
|
|
|
recoveryMode bool
|
|
|
|
clusterNetworkLayer cluster.NetworkLayer
|
|
|
|
// PR1103disabled is used to test upgrade workflows: when set to true,
|
|
// the correct behaviour for namespaced cubbyholes is disabled, so we
|
|
// can test an upgrade to a version that includes the fixes from
|
|
// https://github.com/hashicorp/vault-enterprise/pull/1103
|
|
PR1103disabled bool
|
|
|
|
quotaManager *quotas.Manager
|
|
|
|
clusterHeartbeatInterval time.Duration
|
|
|
|
activityLogConfig ActivityLogCoreConfig
|
|
|
|
// activeTime is set on active nodes indicating the time at which this node
|
|
// became active.
|
|
activeTime time.Time
|
|
|
|
// KeyRotateGracePeriod is how long we allow an upgrade path
|
|
// for standby instances before we delete the upgrade keys
|
|
keyRotateGracePeriod *int64
|
|
|
|
autoRotateCancel context.CancelFunc
|
|
|
|
// number of workers to use for lease revocation in the expiration manager
|
|
numExpirationWorkers int
|
|
|
|
IndexHeaderHMACKey uberAtomic.Value
|
|
|
|
// disableAutopilot is used to disable the autopilot subsystem in raft storage
|
|
disableAutopilot bool
|
|
|
|
// enable/disable identifying response headers
|
|
enableResponseHeaderHostname bool
|
|
enableResponseHeaderRaftNodeID bool
|
|
|
|
// disableSSCTokens is used to disable server side consistent token creation/usage
|
|
disableSSCTokens bool
|
|
|
|
// versionHistory is a map of vault versions to VaultVersion. The
|
|
// VaultVersion.TimestampInstalled when the version will denote when the version
|
|
// was first run. Note that because perf standbys should be upgraded first, and
|
|
// only the active node will actually write the new version timestamp, a perf
|
|
// standby shouldn't rely on the stored version timestamps being present.
|
|
versionHistory map[string]VaultVersion
|
|
|
|
// effectiveSDKVersion contains the SDK version that standby nodes should use when
|
|
// heartbeating with the active node. Default to the current SDK version.
|
|
effectiveSDKVersion string
|
|
|
|
rollbackPeriod time.Duration
|
|
}
|
|
|
|
func (c *Core) HAState() consts.HAState {
|
|
switch {
|
|
case c.perfStandby:
|
|
return consts.PerfStandby
|
|
case c.standby:
|
|
return consts.Standby
|
|
default:
|
|
return consts.Active
|
|
}
|
|
}
|
|
|
|
// CoreConfig is used to parameterize a core
|
|
type CoreConfig struct {
|
|
entCoreConfig
|
|
|
|
DevToken string
|
|
|
|
BuiltinRegistry BuiltinRegistry
|
|
|
|
LogicalBackends map[string]logical.Factory
|
|
|
|
CredentialBackends map[string]logical.Factory
|
|
|
|
AuditBackends map[string]audit.Factory
|
|
|
|
Physical physical.Backend
|
|
|
|
StorageType string
|
|
|
|
// May be nil, which disables HA operations
|
|
HAPhysical physical.HABackend
|
|
|
|
ServiceRegistration sr.ServiceRegistration
|
|
|
|
// Seal is the configured seal, or if none is configured explicitly, a
|
|
// shamir seal. In migration scenarios this is the new seal.
|
|
Seal Seal
|
|
|
|
// Unwrap seal is the optional seal marked "disabled"; this is the old
|
|
// seal in migration scenarios.
|
|
UnwrapSeal Seal
|
|
|
|
SecureRandomReader io.Reader
|
|
|
|
LogLevel string
|
|
|
|
Logger log.Logger
|
|
|
|
// Disables the trace display for Sentinel checks
|
|
DisableSentinelTrace bool
|
|
|
|
// Disables the LRU cache on the physical backend
|
|
DisableCache bool
|
|
|
|
// Disables mlock syscall
|
|
DisableMlock bool
|
|
|
|
// Custom cache size for the LRU cache on the physical backend, or zero for default
|
|
CacheSize int
|
|
|
|
// Set as the leader address for HA
|
|
RedirectAddr string
|
|
|
|
// Set as the cluster address for HA
|
|
ClusterAddr string
|
|
|
|
DefaultLeaseTTL time.Duration
|
|
|
|
MaxLeaseTTL time.Duration
|
|
|
|
ClusterName string
|
|
|
|
ClusterCipherSuites string
|
|
|
|
EnableUI bool
|
|
|
|
// Enable the raw endpoint
|
|
EnableRaw bool
|
|
|
|
PluginDirectory string
|
|
|
|
PluginFileUid int
|
|
|
|
PluginFilePermissions int
|
|
|
|
DisableSealWrap bool
|
|
|
|
RawConfig *server.Config
|
|
|
|
ReloadFuncs *map[string][]reloadutil.ReloadFunc
|
|
ReloadFuncsLock *sync.RWMutex
|
|
|
|
// Licensing
|
|
License string
|
|
LicensePath string
|
|
LicensingConfig *LicensingConfig
|
|
|
|
DisablePerformanceStandby bool
|
|
DisableIndexing bool
|
|
DisableKeyEncodingChecks bool
|
|
|
|
AllLoggers []log.Logger
|
|
|
|
// Telemetry objects
|
|
MetricsHelper *metricsutil.MetricsHelper
|
|
MetricSink *metricsutil.ClusterMetricSink
|
|
|
|
RecoveryMode bool
|
|
|
|
ClusterNetworkLayer cluster.NetworkLayer
|
|
|
|
ClusterHeartbeatInterval time.Duration
|
|
|
|
// Activity log controls
|
|
ActivityLogConfig ActivityLogCoreConfig
|
|
|
|
// number of workers to use for lease revocation in the expiration manager
|
|
NumExpirationWorkers int
|
|
|
|
// DisableAutopilot is used to disable autopilot subsystem in raft storage
|
|
DisableAutopilot bool
|
|
|
|
// Whether to send headers in the HTTP response showing hostname or raft node ID
|
|
EnableResponseHeaderHostname bool
|
|
EnableResponseHeaderRaftNodeID bool
|
|
|
|
// DisableSSCTokens is used to disable the use of server side consistent tokens
|
|
DisableSSCTokens bool
|
|
|
|
EffectiveSDKVersion string
|
|
|
|
RollbackPeriod time.Duration
|
|
}
|
|
|
|
// GetServiceRegistration returns the config's ServiceRegistration, or nil if it does
|
|
// not exist.
|
|
func (c *CoreConfig) GetServiceRegistration() sr.ServiceRegistration {
|
|
// Check whether there is a ServiceRegistration explicitly configured
|
|
if c.ServiceRegistration != nil {
|
|
return c.ServiceRegistration
|
|
}
|
|
|
|
// Check if HAPhysical is configured and implements ServiceRegistration
|
|
if c.HAPhysical != nil && c.HAPhysical.HAEnabled() {
|
|
if disc, ok := c.HAPhysical.(sr.ServiceRegistration); ok {
|
|
return disc
|
|
}
|
|
}
|
|
|
|
// No service discovery is available.
|
|
return nil
|
|
}
|
|
|
|
// CreateCore conducts static validations on the Core Config
|
|
// and returns an uninitialized core.
|
|
func CreateCore(conf *CoreConfig) (*Core, error) {
|
|
if conf.HAPhysical != nil && conf.HAPhysical.HAEnabled() {
|
|
if conf.RedirectAddr == "" {
|
|
return nil, fmt.Errorf("missing API address, please set in configuration or via environment")
|
|
}
|
|
}
|
|
|
|
if conf.DefaultLeaseTTL == 0 {
|
|
conf.DefaultLeaseTTL = defaultLeaseTTL
|
|
}
|
|
if conf.MaxLeaseTTL == 0 {
|
|
conf.MaxLeaseTTL = maxLeaseTTL
|
|
}
|
|
if conf.DefaultLeaseTTL > conf.MaxLeaseTTL {
|
|
return nil, fmt.Errorf("cannot have DefaultLeaseTTL larger than MaxLeaseTTL")
|
|
}
|
|
|
|
// Validate the advertise addr if its given to us
|
|
if conf.RedirectAddr != "" {
|
|
u, err := url.Parse(conf.RedirectAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("redirect address is not valid url: %w", err)
|
|
}
|
|
|
|
if u.Scheme == "" {
|
|
return nil, fmt.Errorf("redirect address must include scheme (ex. 'http')")
|
|
}
|
|
}
|
|
|
|
// Make a default logger if not provided
|
|
if conf.Logger == nil {
|
|
conf.Logger = logging.NewVaultLogger(log.Trace)
|
|
}
|
|
|
|
// Make a default metric sink if not provided
|
|
if conf.MetricSink == nil {
|
|
conf.MetricSink = metricsutil.BlackholeSink()
|
|
}
|
|
|
|
// Instantiate a non-nil raw config if none is provided
|
|
if conf.RawConfig == nil {
|
|
conf.RawConfig = new(server.Config)
|
|
}
|
|
|
|
// secureRandomReader cannot be nil
|
|
if conf.SecureRandomReader == nil {
|
|
conf.SecureRandomReader = rand.Reader
|
|
}
|
|
|
|
clusterHeartbeatInterval := conf.ClusterHeartbeatInterval
|
|
if clusterHeartbeatInterval == 0 {
|
|
clusterHeartbeatInterval = 5 * time.Second
|
|
}
|
|
|
|
if conf.NumExpirationWorkers == 0 {
|
|
conf.NumExpirationWorkers = numExpirationWorkersDefault
|
|
}
|
|
|
|
effectiveSDKVersion := conf.EffectiveSDKVersion
|
|
if effectiveSDKVersion == "" {
|
|
effectiveSDKVersion = version.GetVersion().Version
|
|
}
|
|
|
|
// Setup the core
|
|
c := &Core{
|
|
entCore: entCore{},
|
|
devToken: conf.DevToken,
|
|
physical: conf.Physical,
|
|
serviceRegistration: conf.GetServiceRegistration(),
|
|
underlyingPhysical: conf.Physical,
|
|
storageType: conf.StorageType,
|
|
redirectAddr: conf.RedirectAddr,
|
|
clusterAddr: new(atomic.Value),
|
|
clusterListener: new(atomic.Value),
|
|
customListenerHeader: new(atomic.Value),
|
|
seal: conf.Seal,
|
|
router: NewRouter(),
|
|
sealed: new(uint32),
|
|
sealMigrationDone: new(uint32),
|
|
standby: true,
|
|
standbyStopCh: new(atomic.Value),
|
|
baseLogger: conf.Logger,
|
|
logger: conf.Logger.Named("core"),
|
|
logLevel: conf.LogLevel,
|
|
|
|
defaultLeaseTTL: conf.DefaultLeaseTTL,
|
|
maxLeaseTTL: conf.MaxLeaseTTL,
|
|
sentinelTraceDisabled: conf.DisableSentinelTrace,
|
|
cachingDisabled: conf.DisableCache,
|
|
clusterName: conf.ClusterName,
|
|
clusterNetworkLayer: conf.ClusterNetworkLayer,
|
|
clusterPeerClusterAddrsCache: cache.New(3*clusterHeartbeatInterval, time.Second),
|
|
enableMlock: !conf.DisableMlock,
|
|
rawEnabled: conf.EnableRaw,
|
|
shutdownDoneCh: make(chan struct{}),
|
|
replicationState: new(uint32),
|
|
atomicPrimaryClusterAddrs: new(atomic.Value),
|
|
atomicPrimaryFailoverAddrs: new(atomic.Value),
|
|
localClusterPrivateKey: new(atomic.Value),
|
|
localClusterCert: new(atomic.Value),
|
|
localClusterParsedCert: new(atomic.Value),
|
|
activeNodeReplicationState: new(uint32),
|
|
keepHALockOnStepDown: new(uint32),
|
|
replicationFailure: new(uint32),
|
|
disablePerfStandby: true,
|
|
activeContextCancelFunc: new(atomic.Value),
|
|
allLoggers: conf.AllLoggers,
|
|
builtinRegistry: conf.BuiltinRegistry,
|
|
neverBecomeActive: new(uint32),
|
|
clusterLeaderParams: new(atomic.Value),
|
|
metricsHelper: conf.MetricsHelper,
|
|
metricSink: conf.MetricSink,
|
|
secureRandomReader: conf.SecureRandomReader,
|
|
rawConfig: new(atomic.Value),
|
|
recoveryMode: conf.RecoveryMode,
|
|
postUnsealStarted: new(uint32),
|
|
raftInfo: new(atomic.Value),
|
|
raftJoinDoneCh: make(chan struct{}),
|
|
clusterHeartbeatInterval: clusterHeartbeatInterval,
|
|
activityLogConfig: conf.ActivityLogConfig,
|
|
keyRotateGracePeriod: new(int64),
|
|
numExpirationWorkers: conf.NumExpirationWorkers,
|
|
raftFollowerStates: raft.NewFollowerStates(),
|
|
disableAutopilot: conf.DisableAutopilot,
|
|
enableResponseHeaderHostname: conf.EnableResponseHeaderHostname,
|
|
enableResponseHeaderRaftNodeID: conf.EnableResponseHeaderRaftNodeID,
|
|
mountMigrationTracker: &sync.Map{},
|
|
disableSSCTokens: conf.DisableSSCTokens,
|
|
effectiveSDKVersion: effectiveSDKVersion,
|
|
userFailedLoginInfo: make(map[FailedLoginUser]*FailedLoginInfo),
|
|
}
|
|
|
|
c.standbyStopCh.Store(make(chan struct{}))
|
|
atomic.StoreUint32(c.sealed, 1)
|
|
c.metricSink.SetGaugeWithLabels([]string{"core", "unsealed"}, 0, nil)
|
|
|
|
c.allLoggers = append(c.allLoggers, c.logger)
|
|
|
|
c.router.logger = c.logger.Named("router")
|
|
c.allLoggers = append(c.allLoggers, c.router.logger)
|
|
|
|
c.inFlightReqData = &InFlightRequests{
|
|
InFlightReqMap: &sync.Map{},
|
|
InFlightReqCount: uberAtomic.NewUint64(0),
|
|
}
|
|
|
|
c.SetConfig(conf.RawConfig)
|
|
|
|
atomic.StoreUint32(c.replicationState, uint32(consts.ReplicationDRDisabled|consts.ReplicationPerformanceDisabled))
|
|
c.localClusterCert.Store(([]byte)(nil))
|
|
c.localClusterParsedCert.Store((*x509.Certificate)(nil))
|
|
c.localClusterPrivateKey.Store((*ecdsa.PrivateKey)(nil))
|
|
|
|
c.clusterLeaderParams.Store((*ClusterLeaderParams)(nil))
|
|
c.clusterAddr.Store(conf.ClusterAddr)
|
|
c.activeContextCancelFunc.Store((context.CancelFunc)(nil))
|
|
atomic.StoreInt64(c.keyRotateGracePeriod, int64(2*time.Minute))
|
|
|
|
c.hcpLinkStatus = HCPLinkStatus{
|
|
lock: sync.RWMutex{},
|
|
ConnectionStatus: "disconnected",
|
|
}
|
|
|
|
c.raftInfo.Store((*raftInformation)(nil))
|
|
|
|
switch conf.ClusterCipherSuites {
|
|
case "tls13", "tls12":
|
|
// Do nothing, let Go use the default
|
|
|
|
case "":
|
|
// Add in forward compatible TLS 1.3 suites, followed by handpicked 1.2 suites
|
|
c.clusterCipherSuites = []uint16{
|
|
// 1.3
|
|
tls.TLS_AES_128_GCM_SHA256,
|
|
tls.TLS_AES_256_GCM_SHA384,
|
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
|
// 1.2
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
}
|
|
|
|
default:
|
|
suites, err := tlsutil.ParseCiphers(conf.ClusterCipherSuites)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing cluster cipher suites: %w", err)
|
|
}
|
|
c.clusterCipherSuites = suites
|
|
}
|
|
|
|
// Load CORS config and provide a value for the core field.
|
|
c.corsConfig = &CORSConfig{
|
|
core: c,
|
|
Enabled: new(uint32),
|
|
}
|
|
|
|
if c.seal == nil {
|
|
wrapper := aeadwrapper.NewShamirWrapper()
|
|
wrapper.SetConfig(context.Background(), awskms.WithLogger(c.logger.Named("shamir")))
|
|
|
|
c.seal = NewDefaultSeal(&vaultseal.Access{
|
|
Wrapper: wrapper,
|
|
})
|
|
}
|
|
c.seal.SetCore(c)
|
|
return c, nil
|
|
}
|
|
|
|
// NewCore is used to construct a new core
|
|
func NewCore(conf *CoreConfig) (*Core, error) {
|
|
var err error
|
|
c, err := CreateCore(conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = coreInit(c, conf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !conf.DisableMlock {
|
|
// Ensure our memory usage is locked into physical RAM
|
|
if err := mlock.LockMemory(); err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Failed to lock memory: %v\n\n"+
|
|
"This usually means that the mlock syscall is not available.\n"+
|
|
"Vault uses mlock to prevent memory from being swapped to\n"+
|
|
"disk. This requires root privileges as well as a machine\n"+
|
|
"that supports mlock. Please enable mlock on your system or\n"+
|
|
"disable Vault from using it. To disable Vault from using it,\n"+
|
|
"set the `disable_mlock` configuration option in your configuration\n"+
|
|
"file.",
|
|
err)
|
|
}
|
|
}
|
|
|
|
// Construct a new AES-GCM barrier
|
|
c.barrier, err = NewAESGCMBarrier(c.physical)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("barrier setup failed: %w", err)
|
|
}
|
|
|
|
if err := storedLicenseCheck(c, conf); err != nil {
|
|
return nil, err
|
|
}
|
|
// We create the funcs here, then populate the given config with it so that
|
|
// the caller can share state
|
|
conf.ReloadFuncsLock = &c.reloadFuncsLock
|
|
c.reloadFuncsLock.Lock()
|
|
c.reloadFuncs = make(map[string][]reloadutil.ReloadFunc)
|
|
c.reloadFuncsLock.Unlock()
|
|
conf.ReloadFuncs = &c.reloadFuncs
|
|
|
|
c.rollbackPeriod = conf.RollbackPeriod
|
|
if conf.RollbackPeriod == 0 {
|
|
c.rollbackPeriod = time.Minute
|
|
}
|
|
|
|
// All the things happening below this are not required in
|
|
// recovery mode
|
|
if c.recoveryMode {
|
|
return c, nil
|
|
}
|
|
|
|
if conf.PluginDirectory != "" {
|
|
c.pluginDirectory, err = filepath.Abs(conf.PluginDirectory)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("core setup failed, could not verify plugin directory: %w", err)
|
|
}
|
|
}
|
|
|
|
if conf.PluginFileUid != 0 {
|
|
c.pluginFileUid = conf.PluginFileUid
|
|
}
|
|
if conf.PluginFilePermissions != 0 {
|
|
c.pluginFilePermissions = conf.PluginFilePermissions
|
|
}
|
|
|
|
createSecondaries(c, conf)
|
|
|
|
if conf.HAPhysical != nil && conf.HAPhysical.HAEnabled() {
|
|
c.ha = conf.HAPhysical
|
|
}
|
|
|
|
c.loginMFABackend = NewLoginMFABackend(c, conf.Logger)
|
|
|
|
if c.loginMFABackend.mfaLogger != nil {
|
|
c.AddLogger(c.loginMFABackend.mfaLogger)
|
|
}
|
|
|
|
logicalBackends := make(map[string]logical.Factory)
|
|
for k, f := range conf.LogicalBackends {
|
|
logicalBackends[k] = f
|
|
}
|
|
_, ok := logicalBackends["kv"]
|
|
if !ok {
|
|
logicalBackends["kv"] = PassthroughBackendFactory
|
|
}
|
|
|
|
logicalBackends["cubbyhole"] = CubbyholeBackendFactory
|
|
logicalBackends[systemMountType] = func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) {
|
|
sysBackendLogger := conf.Logger.Named("system")
|
|
c.AddLogger(sysBackendLogger)
|
|
b := NewSystemBackend(c, sysBackendLogger)
|
|
if err := b.Setup(ctx, config); err != nil {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
logicalBackends["identity"] = func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) {
|
|
identityLogger := conf.Logger.Named("identity")
|
|
c.AddLogger(identityLogger)
|
|
return NewIdentityStore(ctx, c, config, identityLogger)
|
|
}
|
|
addExtraLogicalBackends(c, logicalBackends)
|
|
c.logicalBackends = logicalBackends
|
|
|
|
credentialBackends := make(map[string]logical.Factory)
|
|
for k, f := range conf.CredentialBackends {
|
|
credentialBackends[k] = f
|
|
}
|
|
credentialBackends["token"] = func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) {
|
|
tsLogger := conf.Logger.Named("token")
|
|
c.AddLogger(tsLogger)
|
|
return NewTokenStore(ctx, tsLogger, c, config)
|
|
}
|
|
addExtraCredentialBackends(c, credentialBackends)
|
|
c.credentialBackends = credentialBackends
|
|
|
|
auditBackends := make(map[string]audit.Factory)
|
|
for k, f := range conf.AuditBackends {
|
|
auditBackends[k] = f
|
|
}
|
|
c.auditBackends = auditBackends
|
|
|
|
uiStoragePrefix := systemBarrierPrefix + "ui"
|
|
c.uiConfig = NewUIConfig(conf.EnableUI, physical.NewView(c.physical, uiStoragePrefix), NewBarrierView(c.barrier, uiStoragePrefix))
|
|
|
|
c.clusterListener.Store((*cluster.Listener)(nil))
|
|
|
|
// for listeners with custom response headers, configuring customListenerHeader
|
|
if conf.RawConfig.Listeners != nil {
|
|
uiHeaders, err := c.UIHeaders()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.customListenerHeader.Store(NewListenerCustomHeader(conf.RawConfig.Listeners, c.logger, uiHeaders))
|
|
} else {
|
|
c.customListenerHeader.Store(([]*ListenerCustomHeaders)(nil))
|
|
}
|
|
|
|
logRequestsLevel := conf.RawConfig.LogRequestsLevel
|
|
c.logRequestsLevel = uberAtomic.NewInt32(0)
|
|
switch {
|
|
case log.LevelFromString(logRequestsLevel) > log.NoLevel && log.LevelFromString(logRequestsLevel) < log.Off:
|
|
c.logRequestsLevel.Store(int32(log.LevelFromString(logRequestsLevel)))
|
|
case logRequestsLevel != "":
|
|
c.logger.Warn("invalid log_requests_level", "level", conf.RawConfig.LogRequestsLevel)
|
|
}
|
|
|
|
quotasLogger := conf.Logger.Named("quotas")
|
|
c.allLoggers = append(c.allLoggers, quotasLogger)
|
|
c.quotaManager, err = quotas.NewManager(quotasLogger, c.quotaLeaseWalker, c.metricSink)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = c.adjustForSealMigration(conf.UnwrapSeal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.versionHistory == nil {
|
|
c.logger.Info("Initializing version history cache for core")
|
|
c.versionHistory = make(map[string]VaultVersion)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// handleVersionTimeStamps stores the current version at the current time to
|
|
// storage, and then loads all versions and upgrade timestamps out from storage.
|
|
func (c *Core) handleVersionTimeStamps(ctx context.Context) error {
|
|
currentTime := time.Now().UTC()
|
|
|
|
vaultVersion := &VaultVersion{
|
|
TimestampInstalled: currentTime,
|
|
Version: version.Version,
|
|
BuildDate: version.BuildDate,
|
|
}
|
|
|
|
isUpdated, err := c.storeVersionEntry(ctx, vaultVersion, false)
|
|
if err != nil {
|
|
return fmt.Errorf("error storing vault version: %w", err)
|
|
}
|
|
if isUpdated {
|
|
c.logger.Info("Recorded vault version", "vault version", version.Version, "upgrade time", currentTime, "build date", version.BuildDate)
|
|
}
|
|
// Finally, populate the version history cache
|
|
err = c.loadVersionHistory(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HostnameHeaderEnabled determines whether to add the X-Vault-Hostname header
|
|
// to HTTP responses.
|
|
func (c *Core) HostnameHeaderEnabled() bool {
|
|
return c.enableResponseHeaderHostname
|
|
}
|
|
|
|
// RaftNodeIDHeaderEnabled determines whether to add the X-Vault-Raft-Node-ID header
|
|
// to HTTP responses.
|
|
func (c *Core) RaftNodeIDHeaderEnabled() bool {
|
|
return c.enableResponseHeaderRaftNodeID
|
|
}
|
|
|
|
// DisableSSCTokens determines whether to use server side consistent tokens or not.
|
|
func (c *Core) DisableSSCTokens() bool {
|
|
return c.disableSSCTokens
|
|
}
|
|
|
|
// Shutdown is invoked when the Vault instance is about to be terminated. It
|
|
// should not be accessible as part of an API call as it will cause an availability
|
|
// problem. It is only used to gracefully quit in the case of HA so that failover
|
|
// happens as quickly as possible.
|
|
func (c *Core) Shutdown() error {
|
|
c.logger.Debug("shutdown called")
|
|
err := c.sealInternal()
|
|
|
|
c.stateLock.Lock()
|
|
defer c.stateLock.Unlock()
|
|
if c.shutdownDoneCh != nil {
|
|
close(c.shutdownDoneCh)
|
|
c.shutdownDoneCh = nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (c *Core) ShutdownWait() error {
|
|
donech := c.ShutdownDone()
|
|
err := c.Shutdown()
|
|
if donech != nil {
|
|
<-donech
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ShutdownDone returns a channel that will be closed after Shutdown completes
|
|
func (c *Core) ShutdownDone() <-chan struct{} {
|
|
return c.shutdownDoneCh
|
|
}
|
|
|
|
// CORSConfig returns the current CORS configuration
|
|
func (c *Core) CORSConfig() *CORSConfig {
|
|
return c.corsConfig
|
|
}
|
|
|
|
func (c *Core) GetContext() (context.Context, context.CancelFunc) {
|
|
c.stateLock.RLock()
|
|
defer c.stateLock.RUnlock()
|
|
|
|
return context.WithCancel(namespace.RootContext(c.activeContext))
|
|
}
|
|
|
|
// Sealed checks if the Vault is current sealed
|
|
func (c *Core) Sealed() bool {
|
|
return atomic.LoadUint32(c.sealed) == 1
|
|
}
|
|
|
|
// SecretProgress returns the number of keys provided so far
|
|
func (c *Core) SecretProgress() (int, string) {
|
|
c.stateLock.RLock()
|
|
defer c.stateLock.RUnlock()
|
|
switch c.unlockInfo {
|
|
case nil:
|
|
return 0, ""
|
|
default:
|
|
return len(c.unlockInfo.Parts), c.unlockInfo.Nonce
|
|
}
|
|
}
|
|
|
|
// ResetUnsealProcess removes the current unlock parts from memory, to reset
|
|
// the unsealing process
|
|
func (c *Core) ResetUnsealProcess() {
|
|
c.stateLock.Lock()
|
|
defer c.stateLock.Unlock()
|
|
c.unlockInfo = nil
|
|
}
|
|
|
|
func (c *Core) UnsealMigrate(key []byte) (bool, error) {
|
|
err := c.unsealFragment(key, true)
|
|
return !c.Sealed(), err
|
|
}
|
|
|
|
// Unseal is used to provide one of the key parts to unseal the Vault.
|
|
func (c *Core) Unseal(key []byte) (bool, error) {
|
|
err := c.unsealFragment(key, false)
|
|
return !c.Sealed(), err
|
|
}
|
|
|
|
// unseal takes a key fragment and attempts to use it to unseal Vault.
|
|
// Vault may remain sealed afterwards even when no error is returned,
|
|
// depending on whether enough key fragments were provided to meet the
|
|
// target threshold.
|
|
//
|
|
// The provided key should be a recovery key fragment if the seal
|
|
// is an autoseal, or a regular seal key fragment for shamir. In
|
|
// migration scenarios "seal" in the preceding sentence refers to
|
|
// the migration seal in c.migrationInfo.seal.
|
|
//
|
|
// We use getUnsealKey to work out if we have enough fragments,
|
|
// and if we don't have enough we return early. Otherwise we get
|
|
// back the combined key.
|
|
//
|
|
// For legacy shamir the combined key *is* the master key. For
|
|
// shamir the combined key is used to decrypt the master key
|
|
// read from storage. For autoseal the combined key isn't used
|
|
// except to verify that the stored recovery key matches.
|
|
//
|
|
// In migration scenarios a side-effect of unsealing is that
|
|
// the members of c.migrationInfo are populated (excluding
|
|
// .seal, which must already be populated before unseal is called.)
|
|
func (c *Core) unsealFragment(key []byte, migrate bool) error {
|
|
defer metrics.MeasureSince([]string{"core", "unseal"}, time.Now())
|
|
|
|
c.stateLock.Lock()
|
|
defer c.stateLock.Unlock()
|
|
|
|
ctx := context.Background()
|
|
|
|
if migrate && c.migrationInfo == nil {
|
|
return fmt.Errorf("can't perform a seal migration, no migration seal found")
|
|
}
|
|
if migrate && c.isRaftUnseal() {
|
|
return fmt.Errorf("can't perform a seal migration while joining a raft cluster")
|
|
}
|
|
if !migrate && c.migrationInfo != nil {
|
|
done, err := c.sealMigrated(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error checking to see if seal is migrated: %w", err)
|
|
}
|
|
if !done {
|
|
return fmt.Errorf("migrate option not provided and seal migration is pending")
|
|
}
|
|
}
|
|
|
|
c.logger.Debug("unseal key supplied", "migrate", migrate)
|
|
|
|
// Explicitly check for init status. This also checks if the seal
|
|
// configuration is valid (i.e. non-nil).
|
|
init, err := c.Initialized(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !init && !c.isRaftUnseal() {
|
|
return ErrNotInit
|
|
}
|
|
|
|
// Verify the key length
|
|
min, max := c.barrier.KeyLength()
|
|
max += shamir.ShareOverhead
|
|
if len(key) < min {
|
|
return &ErrInvalidKey{fmt.Sprintf("key is shorter than minimum %d bytes", min)}
|
|
}
|
|
if len(key) > max {
|
|
return &ErrInvalidKey{fmt.Sprintf("key is longer than maximum %d bytes", max)}
|
|
}
|
|
|
|
// Check if already unsealed
|
|
if !c.Sealed() {
|
|
return nil
|
|
}
|
|
|
|
sealToUse := c.seal
|
|
if migrate {
|
|
c.logger.Info("unsealing using migration seal")
|
|
sealToUse = c.migrationInfo.seal
|
|
}
|
|
|
|
newKey, err := c.recordUnsealPart(key)
|
|
if !newKey || err != nil {
|
|
return err
|
|
}
|
|
|
|
// getUnsealKey returns either a recovery key (in the case of an autoseal)
|
|
// or a master key (legacy shamir) or an unseal key (new-style shamir).
|
|
combinedKey, err := c.getUnsealKey(ctx, sealToUse)
|
|
if err != nil || combinedKey == nil {
|
|
return err
|
|
}
|
|
if migrate {
|
|
c.migrationInfo.unsealKey = combinedKey
|
|
}
|
|
|
|
if c.isRaftUnseal() {
|
|
return c.unsealWithRaft(combinedKey)
|
|
}
|
|
masterKey, err := c.unsealKeyToMasterKeyPreUnseal(ctx, sealToUse, combinedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.unsealInternal(ctx, masterKey)
|
|
}
|
|
|
|
func (c *Core) unsealWithRaft(combinedKey []byte) error {
|
|
ctx := context.Background()
|
|
|
|
if c.seal.BarrierType() == wrapping.WrapperTypeShamir {
|
|
// If this is a legacy shamir seal this serves no purpose but it
|
|
// doesn't hurt.
|
|
err := c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAesGcmKeyBytes(combinedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
raftInfo := c.raftInfo.Load().(*raftInformation)
|
|
|
|
switch raftInfo.joinInProgress {
|
|
case true:
|
|
// JoinRaftCluster is already trying to perform a join based on retry_join configuration.
|
|
// Inform that routine that unseal key validation is complete so that it can continue to
|
|
// try and join possible leader nodes, and wait for it to complete.
|
|
|
|
atomic.StoreUint32(c.postUnsealStarted, 1)
|
|
|
|
c.logger.Info("waiting for raft retry join process to complete")
|
|
<-c.raftJoinDoneCh
|
|
|
|
default:
|
|
// This is the case for manual raft join. Send the answer to the leader node and
|
|
// wait for data to start streaming in.
|
|
if err := c.joinRaftSendAnswer(ctx, c.seal.GetAccess(), raftInfo); err != nil {
|
|
return err
|
|
}
|
|
// Reset the state
|
|
c.raftInfo.Store((*raftInformation)(nil))
|
|
}
|
|
|
|
go func() {
|
|
var masterKey []byte
|
|
keyringFound := false
|
|
|
|
// Wait until we at least have the keyring before we attempt to
|
|
// unseal the node.
|
|
for {
|
|
if !keyringFound {
|
|
keys, err := c.underlyingPhysical.List(ctx, keyringPrefix)
|
|
if err != nil {
|
|
c.logger.Error("failed to list physical keys", "error", err)
|
|
return
|
|
}
|
|
if strutil.StrListContains(keys, "keyring") {
|
|
keyringFound = true
|
|
}
|
|
}
|
|
if keyringFound && len(masterKey) == 0 {
|
|
var err error
|
|
masterKey, err = c.unsealKeyToMasterKeyPreUnseal(ctx, c.seal, combinedKey)
|
|
if err != nil {
|
|
c.logger.Error("failed to read master key", "error", err)
|
|
return
|
|
}
|
|
}
|
|
if keyringFound && len(masterKey) > 0 {
|
|
err := c.unsealInternal(ctx, masterKey)
|
|
if err != nil {
|
|
c.logger.Error("failed to unseal", "error", err)
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// recordUnsealPart takes in a key fragment, and returns true if it's a new fragment.
|
|
func (c *Core) recordUnsealPart(key []byte) (bool, error) {
|
|
// Check if we already have this piece
|
|
if c.unlockInfo != nil {
|
|
for _, existing := range c.unlockInfo.Parts {
|
|
if subtle.ConstantTimeCompare(existing, key) == 1 {
|
|
return false, nil
|
|
}
|
|
}
|
|
} else {
|
|
uuid, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
c.unlockInfo = &unlockInformation{
|
|
Nonce: uuid,
|
|
}
|
|
}
|
|
|
|
// Store this key
|
|
c.unlockInfo.Parts = append(c.unlockInfo.Parts, key)
|
|
return true, nil
|
|
}
|
|
|
|
// getUnsealKey uses key fragments recorded by recordUnsealPart and
|
|
// returns the combined key if the key share threshold is met.
|
|
// If the key fragments are part of a recovery key, also verify that
|
|
// it matches the stored recovery key on disk.
|
|
func (c *Core) getUnsealKey(ctx context.Context, seal Seal) ([]byte, error) {
|
|
var config *SealConfig
|
|
var err error
|
|
|
|
raftInfo := c.raftInfo.Load().(*raftInformation)
|
|
|
|
switch {
|
|
case seal.RecoveryKeySupported():
|
|
config, err = seal.RecoveryConfig(ctx)
|
|
case c.isRaftUnseal():
|
|
// Ignore follower's seal config and refer to leader's barrier
|
|
// configuration.
|
|
config = raftInfo.leaderBarrierConfig
|
|
default:
|
|
config, err = seal.BarrierConfig(ctx)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if config == nil {
|
|
return nil, fmt.Errorf("failed to obtain seal/recovery configuration")
|
|
}
|
|
|
|
// Check if we don't have enough keys to unlock, proceed through the rest of
|
|
// the call only if we have met the threshold
|
|
if len(c.unlockInfo.Parts) < config.SecretThreshold {
|
|
if c.logger.IsDebug() {
|
|
c.logger.Debug("cannot unseal, not enough keys", "keys", len(c.unlockInfo.Parts), "threshold", config.SecretThreshold, "nonce", c.unlockInfo.Nonce)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
defer func() {
|
|
c.unlockInfo = nil
|
|
}()
|
|
|
|
// Recover the split key. recoveredKey is the shamir combined
|
|
// key, or the single provided key if the threshold is 1.
|
|
var unsealKey []byte
|
|
if config.SecretThreshold == 1 {
|
|
unsealKey = make([]byte, len(c.unlockInfo.Parts[0]))
|
|
copy(unsealKey, c.unlockInfo.Parts[0])
|
|
} else {
|
|
unsealKey, err = shamir.Combine(c.unlockInfo.Parts)
|
|
if err != nil {
|
|
return nil, &ErrInvalidKey{fmt.Sprintf("failed to compute combined key: %v", err)}
|
|
}
|
|
}
|
|
|
|
if seal.RecoveryKeySupported() {
|
|
if err := seal.VerifyRecoveryKey(ctx, unsealKey); err != nil {
|
|
return nil, &ErrInvalidKey{fmt.Sprintf("failed to verify recovery key: %v", err)}
|
|
}
|
|
}
|
|
|
|
return unsealKey, nil
|
|
}
|
|
|
|
// sealMigrated must be called with the stateLock held. It returns true if
|
|
// the seal configured in HCL and the seal configured in storage match.
|
|
// For the auto->auto same seal migration scenario, it will return false even
|
|
// if the preceding conditions are true but we cannot decrypt the master key
|
|
// in storage using the configured seal.
|
|
func (c *Core) sealMigrated(ctx context.Context) (bool, error) {
|
|
if atomic.LoadUint32(c.sealMigrationDone) == 1 {
|
|
return true, nil
|
|
}
|
|
|
|
existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if existBarrierSealConfig.Type != c.seal.BarrierType().String() {
|
|
return false, nil
|
|
}
|
|
if c.seal.RecoveryKeySupported() && existRecoverySealConfig.Type != c.seal.RecoveryType() {
|
|
return false, nil
|
|
}
|
|
|
|
if c.seal.BarrierType() != c.migrationInfo.seal.BarrierType() {
|
|
return true, nil
|
|
}
|
|
|
|
// The above checks can handle the auto->shamir and shamir->auto
|
|
// and auto1->auto2 cases. For auto1->auto1, we need to actually try
|
|
// to read and decrypt the keys.
|
|
|
|
keysMig, errMig := c.migrationInfo.seal.GetStoredKeys(ctx)
|
|
keys, err := c.seal.GetStoredKeys(ctx)
|
|
|
|
switch {
|
|
case len(keys) > 0 && err == nil:
|
|
return true, nil
|
|
case len(keysMig) > 0 && errMig == nil:
|
|
return false, nil
|
|
case errors.Is(err, &ErrDecrypt{}) && errors.Is(errMig, &ErrDecrypt{}):
|
|
return false, fmt.Errorf("decrypt error, neither the old nor new seal can read stored keys: old seal err=%v, new seal err=%v", errMig, err)
|
|
default:
|
|
return false, fmt.Errorf("neither the old nor new seal can read stored keys: old seal err=%v, new seal err=%v", errMig, err)
|
|
}
|
|
}
|
|
|
|
// migrateSeal must be called with the stateLock held.
|
|
func (c *Core) migrateSeal(ctx context.Context) error {
|
|
if c.migrationInfo == nil {
|
|
return nil
|
|
}
|
|
|
|
ok, err := c.sealMigrated(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error checking if seal is migrated or not: %w", err)
|
|
}
|
|
|
|
if ok {
|
|
c.logger.Info("migration is already performed")
|
|
return nil
|
|
}
|
|
|
|
c.logger.Info("seal migration initiated")
|
|
|
|
switch {
|
|
case c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported():
|
|
c.logger.Info("migrating from one auto-unseal to another", "from",
|
|
c.migrationInfo.seal.BarrierType(), "to", c.seal.BarrierType())
|
|
|
|
// Set the recovery and barrier keys to be the same.
|
|
recoveryKey, err := c.migrationInfo.seal.RecoveryKey(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting recovery key to set on new seal: %w", err)
|
|
}
|
|
|
|
if err := c.seal.SetRecoveryKey(ctx, recoveryKey); err != nil {
|
|
return fmt.Errorf("error setting new recovery key information during migrate: %w", err)
|
|
}
|
|
|
|
barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting stored keys to set on new seal: %w", err)
|
|
}
|
|
|
|
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil {
|
|
return fmt.Errorf("error setting new barrier key information during migrate: %w", err)
|
|
}
|
|
|
|
case c.migrationInfo.seal.RecoveryKeySupported():
|
|
c.logger.Info("migrating from one auto-unseal to shamir", "from", c.migrationInfo.seal.BarrierType())
|
|
// Auto to Shamir, since recovery key isn't supported on new seal
|
|
|
|
recoveryKey, err := c.migrationInfo.seal.RecoveryKey(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting recovery key to set on new seal: %w", err)
|
|
}
|
|
|
|
// We have recovery keys; we're going to use them as the new shamir KeK.
|
|
err = c.seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAesGcmKeyBytes(recoveryKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set master key in seal: %w", err)
|
|
}
|
|
|
|
barrierKeys, err := c.migrationInfo.seal.GetStoredKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting stored keys to set on new seal: %w", err)
|
|
}
|
|
|
|
if err := c.seal.SetStoredKeys(ctx, barrierKeys); err != nil {
|
|
return fmt.Errorf("error setting new barrier key information during migrate: %w", err)
|
|
}
|
|
|
|
case c.seal.RecoveryKeySupported():
|
|
c.logger.Info("migrating from shamir to auto-unseal", "to", c.seal.BarrierType())
|
|
// Migration is happening from shamir -> auto. In this case use the shamir
|
|
// combined key that was used to store the master key as the new recovery key.
|
|
if err := c.seal.SetRecoveryKey(ctx, c.migrationInfo.unsealKey); err != nil {
|
|
return fmt.Errorf("error setting new recovery key information: %w", err)
|
|
}
|
|
|
|
// Generate a new master key
|
|
newMasterKey, err := c.barrier.GenerateKey(c.secureRandomReader)
|
|
if err != nil {
|
|
return fmt.Errorf("error generating new master key: %w", err)
|
|
}
|
|
|
|
// Rekey the barrier. This handles the case where the shamir seal we're
|
|
// migrating from was a legacy seal without a stored master key.
|
|
if err := c.barrier.Rekey(ctx, newMasterKey); err != nil {
|
|
return fmt.Errorf("error rekeying barrier during migration: %w", err)
|
|
}
|
|
|
|
// Store the new master key
|
|
if err := c.seal.SetStoredKeys(ctx, [][]byte{newMasterKey}); err != nil {
|
|
return fmt.Errorf("error storing new master key: %w", err)
|
|
}
|
|
|
|
default:
|
|
return errors.New("unhandled migration case (shamir to shamir)")
|
|
}
|
|
|
|
err = c.migrateSealConfig(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error storing new seal configs: %w", err)
|
|
}
|
|
|
|
// Flag migration performed for seal-rewrap later
|
|
atomic.StoreUint32(c.sealMigrationDone, 1)
|
|
|
|
c.logger.Info("seal migration complete")
|
|
return nil
|
|
}
|
|
|
|
// unsealInternal takes in the master key and attempts to unseal the barrier.
|
|
// N.B.: This must be called with the state write lock held.
|
|
func (c *Core) unsealInternal(ctx context.Context, masterKey []byte) error {
|
|
// Attempt to unlock
|
|
if err := c.barrier.Unseal(ctx, masterKey); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := preUnsealInternal(ctx, c); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.startClusterListener(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.startRaftBackend(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupReplicationResolverHandler(); err != nil {
|
|
c.logger.Warn("failed to start replication resolver server", "error", err)
|
|
}
|
|
|
|
// Do post-unseal setup if HA is not enabled
|
|
if c.ha == nil {
|
|
// We still need to set up cluster info even if it's not part of a
|
|
// cluster right now. This also populates the cached cluster object.
|
|
if err := c.setupCluster(ctx); err != nil {
|
|
c.logger.Error("cluster setup failed", "error", err)
|
|
c.barrier.Seal()
|
|
c.logger.Warn("vault is sealed")
|
|
return err
|
|
}
|
|
|
|
if err := c.migrateSeal(ctx); err != nil {
|
|
c.logger.Error("seal migration error", "error", err)
|
|
c.barrier.Seal()
|
|
c.logger.Warn("vault is sealed")
|
|
return err
|
|
}
|
|
|
|
ctx, ctxCancel := context.WithCancel(namespace.RootContext(nil))
|
|
if err := c.postUnseal(ctx, ctxCancel, standardUnsealStrategy{}); err != nil {
|
|
c.logger.Error("post-unseal setup failed", "error", err)
|
|
c.barrier.Seal()
|
|
c.logger.Warn("vault is sealed")
|
|
return err
|
|
}
|
|
|
|
// Force a cache bust here, which will also run migration code
|
|
if c.seal.RecoveryKeySupported() {
|
|
c.seal.SetRecoveryConfig(ctx, nil)
|
|
}
|
|
|
|
c.standby = false
|
|
} else {
|
|
// Go to standby mode, wait until we are active to unseal
|
|
c.standbyDoneCh = make(chan struct{})
|
|
c.manualStepDownCh = make(chan struct{}, 1)
|
|
c.standbyStopCh.Store(make(chan struct{}))
|
|
go c.runStandby(c.standbyDoneCh, c.manualStepDownCh, c.standbyStopCh.Load().(chan struct{}))
|
|
}
|
|
|
|
// Success!
|
|
atomic.StoreUint32(c.sealed, 0)
|
|
c.metricSink.SetGaugeWithLabels([]string{"core", "unsealed"}, 1, nil)
|
|
|
|
if c.logger.IsInfo() {
|
|
c.logger.Info("vault is unsealed")
|
|
}
|
|
|
|
if c.serviceRegistration != nil {
|
|
if err := c.serviceRegistration.NotifySealedStateChange(false); err != nil {
|
|
if c.logger.IsWarn() {
|
|
c.logger.Warn("failed to notify unsealed status", "error", err)
|
|
}
|
|
}
|
|
if err := c.serviceRegistration.NotifyInitializedStateChange(true); err != nil {
|
|
if c.logger.IsWarn() {
|
|
c.logger.Warn("failed to notify initialized status", "error", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SealWithRequest takes in a logical.Request, acquires the lock, and passes
|
|
// through to sealInternal
|
|
func (c *Core) SealWithRequest(httpCtx context.Context, req *logical.Request) error {
|
|
defer metrics.MeasureSince([]string{"core", "seal-with-request"}, time.Now())
|
|
|
|
if c.Sealed() {
|
|
return nil
|
|
}
|
|
|
|
c.stateLock.RLock()
|
|
|
|
// This will unlock the read lock
|
|
// We use background context since we may not be active
|
|
ctx, cancel := context.WithCancel(namespace.RootContext(nil))
|
|
defer cancel()
|
|
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-httpCtx.Done():
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
// This will unlock the read lock
|
|
return c.sealInitCommon(ctx, req)
|
|
}
|
|
|
|
// Seal takes in a token and creates a logical.Request, acquires the lock, and
|
|
// passes through to sealInternal
|
|
func (c *Core) Seal(token string) error {
|
|
defer metrics.MeasureSince([]string{"core", "seal"}, time.Now())
|
|
|
|
if c.Sealed() {
|
|
return nil
|
|
}
|
|
|
|
c.stateLock.RLock()
|
|
|
|
req := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "sys/seal",
|
|
ClientToken: token,
|
|
}
|
|
|
|
// This will unlock the read lock
|
|
// We use background context since we may not be active
|
|
return c.sealInitCommon(namespace.RootContext(nil), req)
|
|
}
|
|
|
|
// sealInitCommon is common logic for Seal and SealWithRequest and is used to
|
|
// re-seal the Vault. This requires the Vault to be unsealed again to perform
|
|
// any further operations. Note: this function will read-unlock the state lock.
|
|
func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr error) {
|
|
defer metrics.MeasureSince([]string{"core", "seal-internal"}, time.Now())
|
|
|
|
var unlocked bool
|
|
defer func() {
|
|
if !unlocked {
|
|
c.stateLock.RUnlock()
|
|
}
|
|
}()
|
|
|
|
if req == nil {
|
|
return errors.New("nil request to seal")
|
|
}
|
|
|
|
// Since there is no token store in standby nodes, sealing cannot be done.
|
|
// Ideally, the request has to be forwarded to leader node for validation
|
|
// and the operation should be performed. But for now, just returning with
|
|
// an error and recommending a vault restart, which essentially does the
|
|
// same thing.
|
|
if c.standby {
|
|
c.logger.Error("vault cannot seal when in standby mode; please restart instead")
|
|
return errors.New("vault cannot seal when in standby mode; please restart instead")
|
|
}
|
|
|
|
err := c.PopulateTokenEntry(ctx, req)
|
|
if err != nil {
|
|
if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) {
|
|
return logical.ErrPermissionDenied
|
|
}
|
|
return logical.ErrInvalidRequest
|
|
}
|
|
acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(ctx, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Audit-log the request before going any further
|
|
auth := &logical.Auth{
|
|
ClientToken: req.ClientToken,
|
|
Accessor: req.ClientTokenAccessor,
|
|
}
|
|
if te != nil {
|
|
auth.IdentityPolicies = identityPolicies[te.NamespaceID]
|
|
delete(identityPolicies, te.NamespaceID)
|
|
auth.ExternalNamespacePolicies = identityPolicies
|
|
auth.TokenPolicies = te.Policies
|
|
auth.Policies = append(te.Policies, identityPolicies[te.NamespaceID]...)
|
|
auth.Metadata = te.Meta
|
|
auth.DisplayName = te.DisplayName
|
|
auth.EntityID = te.EntityID
|
|
auth.TokenType = te.Type
|
|
}
|
|
|
|
logInput := &logical.LogInput{
|
|
Auth: auth,
|
|
Request: req,
|
|
}
|
|
if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil {
|
|
c.logger.Error("failed to audit request", "request_path", req.Path, "error", err)
|
|
return errors.New("failed to audit request, cannot continue")
|
|
}
|
|
|
|
if entity != nil && entity.Disabled {
|
|
c.logger.Warn("permission denied as the entity on the token is disabled")
|
|
return logical.ErrPermissionDenied
|
|
}
|
|
if te != nil && te.EntityID != "" && entity == nil {
|
|
c.logger.Warn("permission denied as the entity on the token is invalid")
|
|
return logical.ErrPermissionDenied
|
|
}
|
|
|
|
// Attempt to use the token (decrement num_uses)
|
|
// On error bail out; if the token has been revoked, bail out too
|
|
if te != nil {
|
|
te, err = c.tokenStore.UseToken(ctx, te)
|
|
if err != nil {
|
|
c.logger.Error("failed to use token", "error", err)
|
|
return ErrInternalError
|
|
}
|
|
if te == nil {
|
|
// Token is no longer valid
|
|
return logical.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
// Verify that this operation is allowed
|
|
authResults := c.performPolicyChecks(ctx, acl, te, req, entity, &PolicyCheckOpts{
|
|
RootPrivsRequired: true,
|
|
})
|
|
if !authResults.Allowed {
|
|
retErr = multierror.Append(retErr, authResults.Error)
|
|
if authResults.Error.ErrorOrNil() == nil || authResults.DeniedError {
|
|
retErr = multierror.Append(retErr, logical.ErrPermissionDenied)
|
|
}
|
|
return retErr
|
|
}
|
|
|
|
if te != nil && te.NumUses == tokenRevocationPending {
|
|
// Token needs to be revoked. We do this immediately here because
|
|
// we won't have a token store after sealing.
|
|
leaseID, err := c.expiration.CreateOrFetchRevocationLeaseByToken(c.activeContext, te)
|
|
if err == nil {
|
|
err = c.expiration.Revoke(c.activeContext, leaseID)
|
|
}
|
|
if err != nil {
|
|
c.logger.Error("token needed revocation before seal but failed to revoke", "error", err)
|
|
retErr = multierror.Append(retErr, ErrInternalError)
|
|
}
|
|
}
|
|
|
|
// Unlock; sealing will grab the lock when needed
|
|
unlocked = true
|
|
c.stateLock.RUnlock()
|
|
|
|
sealErr := c.sealInternal()
|
|
|
|
if sealErr != nil {
|
|
retErr = multierror.Append(retErr, sealErr)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// UIEnabled returns if the UI is enabled
|
|
func (c *Core) UIEnabled() bool {
|
|
return c.uiConfig.Enabled()
|
|
}
|
|
|
|
// UIHeaders returns configured UI headers
|
|
func (c *Core) UIHeaders() (http.Header, error) {
|
|
return c.uiConfig.Headers(context.Background())
|
|
}
|
|
|
|
// sealInternal is an internal method used to seal the vault. It does not do
|
|
// any authorization checking.
|
|
func (c *Core) sealInternal() error {
|
|
return c.sealInternalWithOptions(true, false, true)
|
|
}
|
|
|
|
func (c *Core) sealInternalWithOptions(grabStateLock, keepHALock, performCleanup bool) error {
|
|
// Mark sealed, and if already marked return
|
|
if swapped := atomic.CompareAndSwapUint32(c.sealed, 0, 1); !swapped {
|
|
return nil
|
|
}
|
|
c.metricSink.SetGaugeWithLabels([]string{"core", "unsealed"}, 0, nil)
|
|
|
|
c.logger.Info("marked as sealed")
|
|
|
|
// Clear forwarding clients
|
|
c.requestForwardingConnectionLock.Lock()
|
|
c.clearForwardingClients()
|
|
c.requestForwardingConnectionLock.Unlock()
|
|
|
|
activeCtxCancel := c.activeContextCancelFunc.Load().(context.CancelFunc)
|
|
cancelCtxAndLock := func() {
|
|
doneCh := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-doneCh:
|
|
// Attempt to drain any inflight requests
|
|
case <-time.After(DefaultMaxRequestDuration):
|
|
if activeCtxCancel != nil {
|
|
activeCtxCancel()
|
|
}
|
|
}
|
|
}()
|
|
|
|
c.stateLock.Lock()
|
|
close(doneCh)
|
|
// Stop requests from processing
|
|
if activeCtxCancel != nil {
|
|
activeCtxCancel()
|
|
}
|
|
}
|
|
|
|
// Do pre-seal teardown if HA is not enabled
|
|
if c.ha == nil {
|
|
if grabStateLock {
|
|
cancelCtxAndLock()
|
|
defer c.stateLock.Unlock()
|
|
}
|
|
// Even in a non-HA context we key off of this for some things
|
|
c.standby = true
|
|
|
|
// Stop requests from processing
|
|
if activeCtxCancel != nil {
|
|
activeCtxCancel()
|
|
}
|
|
|
|
if err := c.preSeal(); err != nil {
|
|
c.logger.Error("pre-seal teardown failed", "error", err)
|
|
return fmt.Errorf("internal error")
|
|
}
|
|
} else {
|
|
// If we are keeping the lock we already have the state write lock
|
|
// held. Otherwise grab it here so that when stopCh is triggered we are
|
|
// locked.
|
|
if keepHALock {
|
|
atomic.StoreUint32(c.keepHALockOnStepDown, 1)
|
|
}
|
|
if grabStateLock {
|
|
cancelCtxAndLock()
|
|
defer c.stateLock.Unlock()
|
|
}
|
|
|
|
// If we are trying to acquire the lock, force it to return with nil so
|
|
// runStandby will exit
|
|
// If we are active, signal the standby goroutine to shut down and wait
|
|
// for completion. We have the state lock here so nothing else should
|
|
// be toggling standby status.
|
|
close(c.standbyStopCh.Load().(chan struct{}))
|
|
c.logger.Debug("finished triggering standbyStopCh for runStandby")
|
|
|
|
// Wait for runStandby to stop
|
|
<-c.standbyDoneCh
|
|
atomic.StoreUint32(c.keepHALockOnStepDown, 0)
|
|
c.logger.Debug("runStandby done")
|
|
}
|
|
|
|
c.teardownReplicationResolverHandler()
|
|
|
|
// Perform additional cleanup upon sealing.
|
|
if performCleanup {
|
|
if raftBackend := c.getRaftBackend(); raftBackend != nil {
|
|
if err := raftBackend.TeardownCluster(c.getClusterListener()); err != nil {
|
|
c.logger.Error("error stopping storage cluster", "error", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Stop the cluster listener
|
|
c.stopClusterListener()
|
|
}
|
|
|
|
c.logger.Debug("sealing barrier")
|
|
if err := c.barrier.Seal(); err != nil {
|
|
c.logger.Error("error sealing barrier", "error", err)
|
|
return err
|
|
}
|
|
|
|
if c.serviceRegistration != nil {
|
|
if err := c.serviceRegistration.NotifySealedStateChange(true); err != nil {
|
|
if c.logger.IsWarn() {
|
|
c.logger.Warn("failed to notify sealed status", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.quotaManager != nil {
|
|
if err := c.quotaManager.Reset(); err != nil {
|
|
c.logger.Error("error resetting quota manager", "error", err)
|
|
}
|
|
}
|
|
|
|
postSealInternal(c)
|
|
|
|
c.logger.Info("vault is sealed")
|
|
|
|
return nil
|
|
}
|
|
|
|
type UnsealStrategy interface {
|
|
unseal(context.Context, log.Logger, *Core) error
|
|
}
|
|
|
|
type standardUnsealStrategy struct{}
|
|
|
|
func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, c *Core) error {
|
|
// Clear forwarding clients; we're active
|
|
c.requestForwardingConnectionLock.Lock()
|
|
c.clearForwardingClients()
|
|
c.requestForwardingConnectionLock.Unlock()
|
|
|
|
// Mark the active time. We do this first so it can be correlated to the logs
|
|
// for the active startup.
|
|
c.activeTime = time.Now().UTC()
|
|
|
|
if err := postUnsealPhysical(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := enterprisePostUnseal(c, false); err != nil {
|
|
return err
|
|
}
|
|
if !c.ReplicationState().HasState(consts.ReplicationPerformanceSecondary | consts.ReplicationDRSecondary) {
|
|
// Only perf primarys should write feature flags, but we do it by
|
|
// excluding other states so that we don't have to change it when
|
|
// a non-replicated cluster becomes a primary.
|
|
if err := c.persistFeatureFlags(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.autoRotateCancel == nil {
|
|
var autoRotateCtx context.Context
|
|
autoRotateCtx, c.autoRotateCancel = context.WithCancel(c.activeContext)
|
|
go c.autoRotateBarrierLoop(autoRotateCtx)
|
|
}
|
|
|
|
if !c.IsDRSecondary() {
|
|
if err := c.ensureWrappingKey(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := c.handleVersionTimeStamps(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupPluginCatalog(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.loadMounts(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := enterpriseSetupFilteredPaths(c); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupMounts(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := enterpriseSetupAPILock(c, ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupPolicyStore(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupManagedKeyRegistry(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.loadCORSConfig(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.loadCredentials(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := enterpriseSetupFilteredPaths(c); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupCredentials(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupQuotas(ctx, false); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupHeaderHMACKey(ctx, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !c.IsDRSecondary() {
|
|
if err := c.startRollback(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupExpiration(expireLeaseStrategyFairsharing); err != nil {
|
|
return err
|
|
}
|
|
if err := c.loadAudits(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.setupAudits(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := c.loadIdentityStoreArtifacts(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := loadPolicyMFAConfigs(ctx, c); err != nil {
|
|
return err
|
|
}
|
|
c.setupCachedMFAResponseAuth()
|
|
if err := c.loadLoginMFAConfigs(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.setupAuditedHeadersConfig(ctx); err != nil {
|
|
return err
|
|
}
|
|
// not waiting on wg to avoid changing existing behavior
|
|
var wg sync.WaitGroup
|
|
if err := c.setupActivityLog(ctx, &wg); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
c.auditBroker = NewAuditBroker(c.logger)
|
|
}
|
|
|
|
if !c.ReplicationState().HasState(consts.ReplicationPerformanceSecondary | consts.ReplicationDRSecondary) {
|
|
// Cannot do this above, as we need other resources like mounts to be setup
|
|
if err := c.setupPluginReload(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.getClusterListener() != nil && (c.ha != nil || shouldStartClusterListener(c)) {
|
|
if err := c.setupRaftActiveNode(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.startForwarding(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
c.clusterParamsLock.Lock()
|
|
defer c.clusterParamsLock.Unlock()
|
|
if err := startReplication(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.metricsCh = make(chan struct{})
|
|
go c.emitMetricsActiveNode(c.metricsCh)
|
|
|
|
return nil
|
|
}
|
|
|
|
// postUnseal is invoked on the active node, and performance standby nodes,
|
|
// after the barrier is unsealed, but before
|
|
// allowing any user operations. This allows us to setup any state that
|
|
// requires the Vault to be unsealed such as mount tables, logical backends,
|
|
// credential stores, etc.
|
|
func (c *Core) postUnseal(ctx context.Context, ctxCancelFunc context.CancelFunc, unsealer UnsealStrategy) (retErr error) {
|
|
defer metrics.MeasureSince([]string{"core", "post_unseal"}, time.Now())
|
|
|
|
// Clear any out
|
|
c.postUnsealFuncs = nil
|
|
|
|
// Create a new request context
|
|
c.activeContext = ctx
|
|
c.activeContextCancelFunc.Store(ctxCancelFunc)
|
|
|
|
defer func() {
|
|
if retErr != nil {
|
|
ctxCancelFunc()
|
|
_ = c.preSeal()
|
|
}
|
|
}()
|
|
c.logger.Info("post-unseal setup starting")
|
|
|
|
// Enable the cache
|
|
c.physicalCache.Purge(ctx)
|
|
if !c.cachingDisabled {
|
|
c.physicalCache.SetEnabled(true)
|
|
}
|
|
|
|
// Purge these for safety in case of a rekey
|
|
_ = c.seal.SetBarrierConfig(ctx, nil)
|
|
if c.seal.RecoveryKeySupported() {
|
|
_ = c.seal.SetRecoveryConfig(ctx, nil)
|
|
}
|
|
|
|
if err := unsealer.unseal(ctx, c.logger, c); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Automatically re-encrypt the keys used for auto unsealing when the
|
|
// seal's encryption key changes. The regular rotation of cryptographic
|
|
// keys is a NIST recommendation. Access to prior keys for decryption
|
|
// is normally supported for a configurable time period. Re-encrypting
|
|
// the keys used for auto unsealing ensures Vault and its data will
|
|
// continue to be accessible even after prior seal keys are destroyed.
|
|
if seal, ok := c.seal.(*autoSeal); ok {
|
|
if err := seal.UpgradeKeys(c.activeContext); err != nil {
|
|
c.logger.Warn("post-unseal upgrade seal keys failed", "error", err)
|
|
}
|
|
|
|
// Start a periodic but infrequent heartbeat to detect auto-seal backend outages at runtime rather than being
|
|
// surprised by this at the next need to unseal.
|
|
seal.StartHealthCheck()
|
|
}
|
|
|
|
// This is intentionally the last block in this function. We want to allow
|
|
// writes just before allowing client requests, to ensure everything has
|
|
// been set up properly before any writes can have happened.
|
|
for _, v := range c.postUnsealFuncs {
|
|
v()
|
|
}
|
|
|
|
if atomic.LoadUint32(c.sealMigrationDone) == 1 {
|
|
if err := c.postSealMigration(ctx); err != nil {
|
|
c.logger.Warn("post-unseal post seal migration failed", "error", err)
|
|
}
|
|
}
|
|
|
|
if os.Getenv(EnvVaultDisableLocalAuthMountEntities) != "" {
|
|
c.logger.Warn("disabling entities for local auth mounts through env var", "env", EnvVaultDisableLocalAuthMountEntities)
|
|
}
|
|
c.loginMFABackend.usedCodes = cache.New(0, 30*time.Second)
|
|
if c.systemBackend != nil && c.systemBackend.mfaBackend != nil {
|
|
c.systemBackend.mfaBackend.usedCodes = cache.New(0, 30*time.Second)
|
|
}
|
|
c.logger.Info("post-unseal setup complete")
|
|
return nil
|
|
}
|
|
|
|
// preSeal is invoked before the barrier is sealed, allowing
|
|
// for any state teardown required.
|
|
func (c *Core) preSeal() error {
|
|
defer metrics.MeasureSince([]string{"core", "pre_seal"}, time.Now())
|
|
c.logger.Info("pre-seal teardown starting")
|
|
|
|
if seal, ok := c.seal.(*autoSeal); ok {
|
|
seal.StopHealthCheck()
|
|
}
|
|
// Clear any pending funcs
|
|
c.postUnsealFuncs = nil
|
|
c.activeTime = time.Time{}
|
|
|
|
// Clear any rekey progress
|
|
c.barrierRekeyConfig = nil
|
|
c.recoveryRekeyConfig = nil
|
|
|
|
if c.metricsCh != nil {
|
|
close(c.metricsCh)
|
|
c.metricsCh = nil
|
|
}
|
|
var result error
|
|
|
|
c.stopForwarding()
|
|
|
|
c.stopRaftActiveNode()
|
|
|
|
c.clusterParamsLock.Lock()
|
|
if err := stopReplication(c); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error stopping replication: %w", err))
|
|
}
|
|
c.clusterParamsLock.Unlock()
|
|
|
|
if err := c.teardownAudits(); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error tearing down audits: %w", err))
|
|
}
|
|
if err := c.stopExpiration(); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error stopping expiration: %w", err))
|
|
}
|
|
c.stopActivityLog()
|
|
|
|
if err := c.teardownCredentials(context.Background()); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error tearing down credentials: %w", err))
|
|
}
|
|
if err := c.teardownPolicyStore(); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error tearing down policy store: %w", err))
|
|
}
|
|
if err := c.stopRollback(); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error stopping rollback: %w", err))
|
|
}
|
|
if err := c.unloadMounts(context.Background()); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error unloading mounts: %w", err))
|
|
}
|
|
|
|
if err := enterprisePreSeal(c); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
|
|
if c.autoRotateCancel != nil {
|
|
c.autoRotateCancel()
|
|
c.autoRotateCancel = nil
|
|
}
|
|
|
|
if seal, ok := c.seal.(*autoSeal); ok {
|
|
seal.StopHealthCheck()
|
|
}
|
|
|
|
if c.systemBackend != nil && c.systemBackend.mfaBackend != nil {
|
|
c.systemBackend.mfaBackend.usedCodes = nil
|
|
}
|
|
if err := c.teardownLoginMFA(); err != nil {
|
|
result = multierror.Append(result, fmt.Errorf("error tearing down login MFA, error: %w", err))
|
|
}
|
|
|
|
preSealPhysical(c)
|
|
|
|
c.logger.Info("pre-seal teardown complete")
|
|
return result
|
|
}
|
|
|
|
func enterprisePostUnsealImpl(c *Core, isStandby bool) error {
|
|
return nil
|
|
}
|
|
|
|
func enterprisePreSealImpl(c *Core) error {
|
|
return nil
|
|
}
|
|
|
|
func enterpriseSetupFilteredPathsImpl(c *Core) error {
|
|
return nil
|
|
}
|
|
|
|
func enterpriseSetupQuotasImpl(ctx context.Context, c *Core) error {
|
|
return nil
|
|
}
|
|
|
|
func startReplicationImpl(c *Core) error {
|
|
return nil
|
|
}
|
|
|
|
func stopReplicationImpl(c *Core) error {
|
|
return nil
|
|
}
|
|
|
|
func setupAPILockImpl(_ *Core, _ context.Context) error { return nil }
|
|
|
|
func (c *Core) ReplicationState() consts.ReplicationState {
|
|
return consts.ReplicationState(atomic.LoadUint32(c.replicationState))
|
|
}
|
|
|
|
func (c *Core) ActiveNodeReplicationState() consts.ReplicationState {
|
|
return consts.ReplicationState(atomic.LoadUint32(c.activeNodeReplicationState))
|
|
}
|
|
|
|
func (c *Core) SealAccess() *SealAccess {
|
|
return NewSealAccess(c.seal)
|
|
}
|
|
|
|
// StorageType returns a string equal to the storage configuration's type.
|
|
func (c *Core) StorageType() string {
|
|
return c.storageType
|
|
}
|
|
|
|
func (c *Core) Logger() log.Logger {
|
|
return c.logger
|
|
}
|
|
|
|
func (c *Core) BarrierKeyLength() (min, max int) {
|
|
min, max = c.barrier.KeyLength()
|
|
max += shamir.ShareOverhead
|
|
return
|
|
}
|
|
|
|
func (c *Core) AuditedHeadersConfig() *AuditedHeadersConfig {
|
|
return c.auditedHeaders
|
|
}
|
|
|
|
func waitUntilWALShippedImpl(ctx context.Context, c *Core, index uint64) bool {
|
|
return true
|
|
}
|
|
|
|
func merkleRootImpl(c *Core) string {
|
|
return ""
|
|
}
|
|
|
|
func lastWALImpl(c *Core) uint64 {
|
|
return 0
|
|
}
|
|
|
|
func lastPerformanceWALImpl(c *Core) uint64 {
|
|
return 0
|
|
}
|
|
|
|
func lastDRWALImpl(c *Core) uint64 {
|
|
return 0
|
|
}
|
|
|
|
func lastRemoteWALImpl(c *Core) uint64 {
|
|
return 0
|
|
}
|
|
|
|
func lastRemoteUpstreamWALImpl(c *Core) uint64 {
|
|
return 0
|
|
}
|
|
|
|
func (c *Core) PhysicalSealConfigs(ctx context.Context) (*SealConfig, *SealConfig, error) {
|
|
pe, err := c.physical.Get(ctx, barrierSealConfigPath)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch barrier seal configuration at migration check time: %w", err)
|
|
}
|
|
if pe == nil {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
barrierConf := new(SealConfig)
|
|
|
|
if err := jsonutil.DecodeJSON(pe.Value, barrierConf); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to decode barrier seal configuration at migration check time: %w", err)
|
|
}
|
|
err = barrierConf.Validate()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to validate barrier seal configuration at migration check time: %w", err)
|
|
}
|
|
// In older versions of vault the default seal would not store a type. This
|
|
// is here to offer backwards compatibility for older seal configs.
|
|
if barrierConf.Type == "" {
|
|
barrierConf.Type = wrapping.WrapperTypeShamir.String()
|
|
}
|
|
|
|
var recoveryConf *SealConfig
|
|
pe, err = c.physical.Get(ctx, recoverySealConfigPlaintextPath)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch seal configuration at migration check time: %w", err)
|
|
}
|
|
if pe != nil {
|
|
recoveryConf = &SealConfig{}
|
|
if err := jsonutil.DecodeJSON(pe.Value, recoveryConf); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to decode seal configuration at migration check time: %w", err)
|
|
}
|
|
err = recoveryConf.Validate()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to validate seal configuration at migration check time: %w", err)
|
|
}
|
|
// In older versions of vault the default seal would not store a type. This
|
|
// is here to offer backwards compatibility for older seal configs.
|
|
if recoveryConf.Type == "" {
|
|
recoveryConf.Type = wrapping.WrapperTypeShamir.String()
|
|
}
|
|
}
|
|
|
|
return barrierConf, recoveryConf, nil
|
|
}
|
|
|
|
// adjustForSealMigration takes the unwrapSeal, which is nil if (a) we're not
|
|
// configured for seal migration or (b) we might be doing a seal migration away
|
|
// from shamir. It will only be non-nil if there is a configured seal with
|
|
// the config key disabled=true, which implies a migration away from autoseal.
|
|
//
|
|
// For case (a), the common case, we expect that the stored barrier
|
|
// config matches the seal type, in which case we simply return nil. If they
|
|
// don't match, and the stored seal config is of type Shamir but the configured
|
|
// seal is not Shamir, that is case (b) and we make an unwrapSeal of type Shamir.
|
|
// Any other unwrapSeal=nil scenario is treated as an error.
|
|
//
|
|
// Given a non-nil unwrapSeal or case (b), we setup c.migrationInfo to prepare
|
|
// for a migration upon receiving a valid migration unseal request. We cannot
|
|
// check at this time for already performed (or incomplete) migrations because
|
|
// we haven't yet been unsealed, so we have no way of checking whether a
|
|
// shamir seal works to read stored seal-encrypted data.
|
|
//
|
|
// The assumption throughout is that the very last step of seal migration is
|
|
// to write the new barrier/recovery stored seal config.
|
|
func (c *Core) adjustForSealMigration(unwrapSeal Seal) error {
|
|
ctx := context.Background()
|
|
existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("Error checking for existing seal: %s", err)
|
|
}
|
|
|
|
// If we don't have an existing config or if it's the deprecated auto seal
|
|
// which needs an upgrade, skip out
|
|
if existBarrierSealConfig == nil || existBarrierSealConfig.Type == WrapperTypeHsmAutoDeprecated.String() {
|
|
return nil
|
|
}
|
|
|
|
if unwrapSeal == nil {
|
|
// With unwrapSeal==nil, either we're not migrating, or we're migrating
|
|
// from shamir.
|
|
|
|
switch {
|
|
case existBarrierSealConfig.Type == c.seal.BarrierType().String():
|
|
// We have the same barrier type and the unwrap seal is nil so we're not
|
|
// migrating from same to same, IOW we assume it's not a migration.
|
|
return nil
|
|
case c.seal.BarrierType() == wrapping.WrapperTypeShamir:
|
|
// The stored barrier config is not shamir, there is no disabled seal
|
|
// in config, and either no configured seal (which equates to Shamir)
|
|
// or an explicitly configured Shamir seal.
|
|
return fmt.Errorf("cannot seal migrate from %q to Shamir, no disabled seal in configuration",
|
|
existBarrierSealConfig.Type)
|
|
case existBarrierSealConfig.Type == wrapping.WrapperTypeShamir.String():
|
|
// The configured seal is not Shamir, the stored seal config is Shamir.
|
|
// This is a migration away from Shamir.
|
|
unwrapSeal = NewDefaultSeal(&vaultseal.Access{
|
|
Wrapper: aeadwrapper.NewShamirWrapper(),
|
|
})
|
|
default:
|
|
// We know at this point that there is a configured non-Shamir seal,
|
|
// that it does not match the stored non-Shamir seal config, and that
|
|
// there is no explicit disabled seal stanza.
|
|
return fmt.Errorf("cannot seal migrate from %q to %q, no disabled seal in configuration",
|
|
existBarrierSealConfig.Type, c.seal.BarrierType())
|
|
}
|
|
} else {
|
|
// If we're not coming from Shamir we expect the previous seal to be
|
|
// in the config and disabled.
|
|
|
|
if unwrapSeal.BarrierType() == wrapping.WrapperTypeShamir {
|
|
return errors.New("Shamir seals cannot be set disabled (they should simply not be set)")
|
|
}
|
|
}
|
|
|
|
// If we've reached this point it's a migration attempt and we should have both
|
|
// c.migrationInfo.seal (old seal) and c.seal (new seal) populated.
|
|
unwrapSeal.SetCore(c)
|
|
|
|
// No stored recovery seal config found, what about the legacy recovery config?
|
|
if existBarrierSealConfig.Type != wrapping.WrapperTypeShamir.String() && existRecoverySealConfig == nil {
|
|
entry, err := c.physical.Get(ctx, recoverySealConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %q recovery seal configuration: %w", existBarrierSealConfig.Type, err)
|
|
}
|
|
if entry == nil {
|
|
return errors.New("Recovery seal configuration not found for existing seal")
|
|
}
|
|
return errors.New("Cannot migrate seals while using a legacy recovery seal config")
|
|
}
|
|
|
|
c.migrationInfo = &migrationInformation{
|
|
seal: unwrapSeal,
|
|
}
|
|
if existBarrierSealConfig.Type != c.seal.BarrierType().String() {
|
|
// It's unnecessary to call this when doing an auto->auto
|
|
// same-seal-type migration, since they'll have the same configs before
|
|
// and after migration.
|
|
c.adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig)
|
|
}
|
|
c.initSealsForMigration()
|
|
c.logger.Warn("entering seal migration mode; Vault will not automatically unseal even if using an autoseal", "from_barrier_type", c.migrationInfo.seal.BarrierType(), "to_barrier_type", c.seal.BarrierType())
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Core) migrateSealConfig(ctx context.Context) error {
|
|
existBarrierSealConfig, existRecoverySealConfig, err := c.PhysicalSealConfigs(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read existing seal configuration during migration: %v", err)
|
|
}
|
|
|
|
var bc, rc *SealConfig
|
|
|
|
switch {
|
|
case c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported():
|
|
// Migrating from auto->auto, copy the configs over
|
|
bc, rc = existBarrierSealConfig, existRecoverySealConfig
|
|
case c.migrationInfo.seal.RecoveryKeySupported():
|
|
// Migrating from auto->shamir, clone auto's recovery config and set
|
|
// stored keys to 1.
|
|
bc = existRecoverySealConfig.Clone()
|
|
bc.StoredShares = 1
|
|
case c.seal.RecoveryKeySupported():
|
|
// Migrating from shamir->auto, set a new barrier config and set
|
|
// recovery config to a clone of shamir's barrier config with stored
|
|
// keys set to 0.
|
|
bc = &SealConfig{
|
|
Type: c.seal.BarrierType().String(),
|
|
SecretShares: 1,
|
|
SecretThreshold: 1,
|
|
StoredShares: 1,
|
|
}
|
|
|
|
rc = existBarrierSealConfig.Clone()
|
|
rc.StoredShares = 0
|
|
}
|
|
|
|
if err := c.seal.SetBarrierConfig(ctx, bc); err != nil {
|
|
return fmt.Errorf("error storing barrier config after migration: %w", err)
|
|
}
|
|
|
|
if c.seal.RecoveryKeySupported() {
|
|
if err := c.seal.SetRecoveryConfig(ctx, rc); err != nil {
|
|
return fmt.Errorf("error storing recovery config after migration: %w", err)
|
|
}
|
|
} else if err := c.physical.Delete(ctx, recoverySealConfigPlaintextPath); err != nil {
|
|
return fmt.Errorf("failed to delete old recovery seal configuration during migration: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Core) adjustSealConfigDuringMigration(existBarrierSealConfig, existRecoverySealConfig *SealConfig) {
|
|
switch {
|
|
case c.migrationInfo.seal.RecoveryKeySupported() && existRecoverySealConfig != nil:
|
|
// Migrating from auto->shamir, clone auto's recovery config and set
|
|
// stored keys to 1. Unless the recover config doesn't exist, in which
|
|
// case the migration is assumed to already have been performed.
|
|
newSealConfig := existRecoverySealConfig.Clone()
|
|
newSealConfig.StoredShares = 1
|
|
c.seal.SetCachedBarrierConfig(newSealConfig)
|
|
case !c.migrationInfo.seal.RecoveryKeySupported() && c.seal.RecoveryKeySupported():
|
|
// Migrating from shamir->auto, set a new barrier config and set
|
|
// recovery config to a clone of shamir's barrier config with stored
|
|
// keys set to 0.
|
|
newBarrierSealConfig := &SealConfig{
|
|
Type: c.seal.BarrierType().String(),
|
|
SecretShares: 1,
|
|
SecretThreshold: 1,
|
|
StoredShares: 1,
|
|
}
|
|
c.seal.SetCachedBarrierConfig(newBarrierSealConfig)
|
|
|
|
newRecoveryConfig := existBarrierSealConfig.Clone()
|
|
newRecoveryConfig.StoredShares = 0
|
|
c.seal.SetCachedRecoveryConfig(newRecoveryConfig)
|
|
}
|
|
}
|
|
|
|
func (c *Core) unsealKeyToRootKeyPostUnseal(ctx context.Context, combinedKey []byte) ([]byte, error) {
|
|
return c.unsealKeyToMasterKey(ctx, c.seal, combinedKey, true, false)
|
|
}
|
|
|
|
func (c *Core) unsealKeyToMasterKeyPreUnseal(ctx context.Context, seal Seal, combinedKey []byte) ([]byte, error) {
|
|
return c.unsealKeyToMasterKey(ctx, seal, combinedKey, false, true)
|
|
}
|
|
|
|
// unsealKeyToMasterKey takes a key provided by the user, either a recovery key
|
|
// if using an autoseal or an unseal key with Shamir. It returns a nil error
|
|
// if the key is valid and an error otherwise. It also returns the master key
|
|
// that can be used to unseal the barrier.
|
|
// If useTestSeal is true, seal will not be modified; this is used when not
|
|
// invoked as part of an unseal process. Otherwise in the non-legacy shamir
|
|
// case the combinedKey will be set in the seal, which means subsequent attempts
|
|
// to use the seal to read the master key will succeed, assuming combinedKey is
|
|
// valid.
|
|
// If allowMissing is true, a failure to find the master key in storage results
|
|
// in a nil error and a nil master key being returned.
|
|
func (c *Core) unsealKeyToMasterKey(ctx context.Context, seal Seal, combinedKey []byte, useTestSeal bool, allowMissing bool) ([]byte, error) {
|
|
switch seal.StoredKeysSupported() {
|
|
case vaultseal.StoredKeysSupportedGeneric:
|
|
if err := seal.VerifyRecoveryKey(ctx, combinedKey); err != nil {
|
|
return nil, fmt.Errorf("recovery key verification failed: %w", err)
|
|
}
|
|
|
|
storedKeys, err := seal.GetStoredKeys(ctx)
|
|
if storedKeys == nil && err == nil && allowMissing {
|
|
return nil, nil
|
|
}
|
|
|
|
if err == nil && len(storedKeys) != 1 {
|
|
err = fmt.Errorf("expected exactly one stored key, got %d", len(storedKeys))
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to retrieve stored keys: %w", err)
|
|
}
|
|
return storedKeys[0], nil
|
|
|
|
case vaultseal.StoredKeysSupportedShamirRoot:
|
|
if useTestSeal {
|
|
testseal := NewDefaultSeal(&vaultseal.Access{
|
|
Wrapper: aeadwrapper.NewShamirWrapper(),
|
|
})
|
|
testseal.SetCore(c)
|
|
cfg, err := seal.BarrierConfig(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to setup test barrier config: %w", err)
|
|
}
|
|
testseal.SetCachedBarrierConfig(cfg)
|
|
seal = testseal
|
|
}
|
|
|
|
err := seal.GetAccess().Wrapper.(*aeadwrapper.ShamirWrapper).SetAesGcmKeyBytes(combinedKey)
|
|
if err != nil {
|
|
return nil, &ErrInvalidKey{fmt.Sprintf("failed to setup unseal key: %v", err)}
|
|
}
|
|
storedKeys, err := seal.GetStoredKeys(ctx)
|
|
if storedKeys == nil && err == nil && allowMissing {
|
|
return nil, nil
|
|
}
|
|
if err == nil && len(storedKeys) != 1 {
|
|
err = fmt.Errorf("expected exactly one stored key, got %d", len(storedKeys))
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to retrieve stored keys: %w", err)
|
|
}
|
|
return storedKeys[0], nil
|
|
|
|
case vaultseal.StoredKeysNotSupported:
|
|
return combinedKey, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid seal")
|
|
}
|
|
|
|
// IsInSealMigrationMode returns true if we're configured to perform a seal migration,
|
|
// meaning either that we have a disabled seal in HCL configuration or the seal
|
|
// configuration in storage is Shamir but the seal in HCL is not. In this
|
|
// mode we should not auto-unseal (even if the migration is done) and we will
|
|
// accept unseal requests with and without the `migrate` option, though the migrate
|
|
// option is required if we haven't yet performed the seal migration.
|
|
func (c *Core) IsInSealMigrationMode() bool {
|
|
c.stateLock.RLock()
|
|
defer c.stateLock.RUnlock()
|
|
return c.migrationInfo != nil
|
|
}
|
|
|
|
// IsSealMigrated returns true if we're in seal migration mode but migration
|
|
// has already been performed (possibly by another node, or prior to this node's
|
|
// current invocation.)
|
|
func (c *Core) IsSealMigrated() bool {
|
|
if !c.IsInSealMigrationMode() {
|
|
return false
|
|
}
|
|
c.stateLock.RLock()
|
|
defer c.stateLock.RUnlock()
|
|
done, _ := c.sealMigrated(context.Background())
|
|
return done
|
|
}
|
|
|
|
func (c *Core) BarrierEncryptorAccess() *BarrierEncryptorAccess {
|
|
return NewBarrierEncryptorAccess(c.barrier)
|
|
}
|
|
|
|
func (c *Core) PhysicalAccess() *physical.PhysicalAccess {
|
|
return physical.NewPhysicalAccess(c.physical)
|
|
}
|
|
|
|
func (c *Core) RouterAccess() *RouterAccess {
|
|
return NewRouterAccess(c)
|
|
}
|
|
|
|
// IsDRSecondary returns if the current cluster state is a DR secondary.
|
|
func (c *Core) IsDRSecondary() bool {
|
|
return c.ReplicationState().HasState(consts.ReplicationDRSecondary)
|
|
}
|
|
|
|
func (c *Core) IsPerfSecondary() bool {
|
|
return c.ReplicationState().HasState(consts.ReplicationPerformanceSecondary)
|
|
}
|
|
|
|
func (c *Core) AddLogger(logger log.Logger) {
|
|
c.allLoggersLock.Lock()
|
|
defer c.allLoggersLock.Unlock()
|
|
c.allLoggers = append(c.allLoggers, logger)
|
|
}
|
|
|
|
// SetLogLevel sets logging level for all tracked loggers to the level provided
|
|
func (c *Core) SetLogLevel(level log.Level) {
|
|
c.allLoggersLock.RLock()
|
|
defer c.allLoggersLock.RUnlock()
|
|
for _, logger := range c.allLoggers {
|
|
logger.SetLevel(level)
|
|
}
|
|
}
|
|
|
|
// SetLogLevelByName sets the logging level of named logger to level provided
|
|
// if it exists. Core.allLoggers is a slice and as such it is entirely possible
|
|
// that multiple entries exist for the same name. Each instance will be modified.
|
|
func (c *Core) SetLogLevelByName(name string, level log.Level) bool {
|
|
c.allLoggersLock.RLock()
|
|
defer c.allLoggersLock.RUnlock()
|
|
|
|
found := false
|
|
for _, logger := range c.allLoggers {
|
|
if logger.Name() == name {
|
|
logger.SetLevel(level)
|
|
found = true
|
|
}
|
|
}
|
|
|
|
return found
|
|
}
|
|
|
|
// SetConfig sets core's config object to the newly provided config.
|
|
func (c *Core) SetConfig(conf *server.Config) {
|
|
c.rawConfig.Store(conf)
|
|
bz, err := json.Marshal(c.SanitizedConfig())
|
|
if err != nil {
|
|
c.logger.Error("error serializing sanitized config", "error", err)
|
|
return
|
|
}
|
|
|
|
c.logger.Debug("set config", "sanitized config", string(bz))
|
|
}
|
|
|
|
func (c *Core) GetListenerCustomResponseHeaders(listenerAdd string) *ListenerCustomHeaders {
|
|
customHeaders := c.customListenerHeader.Load()
|
|
if customHeaders == nil {
|
|
return nil
|
|
}
|
|
|
|
customHeadersList, ok := customHeaders.([]*ListenerCustomHeaders)
|
|
if customHeadersList == nil || !ok {
|
|
return nil
|
|
}
|
|
|
|
for _, l := range customHeadersList {
|
|
if l.Address == listenerAdd {
|
|
return l
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ExistCustomResponseHeader checks if a custom header is configured in any
|
|
// listener's stanza
|
|
func (c *Core) ExistCustomResponseHeader(header string) bool {
|
|
customHeaders := c.customListenerHeader.Load()
|
|
if customHeaders == nil {
|
|
return false
|
|
}
|
|
|
|
customHeadersList, ok := customHeaders.([]*ListenerCustomHeaders)
|
|
if customHeadersList == nil || !ok {
|
|
return false
|
|
}
|
|
|
|
for _, l := range customHeadersList {
|
|
exist := l.ExistCustomResponseHeader(header)
|
|
if exist {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *Core) ReloadCustomResponseHeaders() error {
|
|
conf := c.rawConfig.Load()
|
|
if conf == nil {
|
|
return fmt.Errorf("failed to load core raw config")
|
|
}
|
|
lns := conf.(*server.Config).Listeners
|
|
if lns == nil {
|
|
return fmt.Errorf("no listener configured")
|
|
}
|
|
|
|
uiHeaders, err := c.UIHeaders()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.customListenerHeader.Store(NewListenerCustomHeader(lns, c.logger, uiHeaders))
|
|
|
|
return nil
|
|
}
|
|
|
|
// SanitizedConfig returns a sanitized version of the current config.
|
|
// See server.Config.Sanitized for specific values omitted.
|
|
func (c *Core) SanitizedConfig() map[string]interface{} {
|
|
conf := c.rawConfig.Load()
|
|
if conf == nil {
|
|
return nil
|
|
}
|
|
return conf.(*server.Config).Sanitized()
|
|
}
|
|
|
|
// LogFormat returns the log format current in use.
|
|
func (c *Core) LogFormat() string {
|
|
conf := c.rawConfig.Load()
|
|
return conf.(*server.Config).LogFormat
|
|
}
|
|
|
|
// MetricsHelper returns the global metrics helper which allows external
|
|
// packages to access Vault's internal metrics.
|
|
func (c *Core) MetricsHelper() *metricsutil.MetricsHelper {
|
|
return c.metricsHelper
|
|
}
|
|
|
|
// MetricSink returns the metrics wrapper with which Core has been configured.
|
|
func (c *Core) MetricSink() *metricsutil.ClusterMetricSink {
|
|
return c.metricSink
|
|
}
|
|
|
|
// BuiltinRegistry is an interface that allows the "vault" package to use
|
|
// the registry of builtin plugins without getting an import cycle. It
|
|
// also allows for mocking the registry easily.
|
|
type BuiltinRegistry interface {
|
|
Contains(name string, pluginType consts.PluginType) bool
|
|
Get(name string, pluginType consts.PluginType) (func() (interface{}, error), bool)
|
|
Keys(pluginType consts.PluginType) []string
|
|
DeprecationStatus(name string, pluginType consts.PluginType) (consts.DeprecationStatus, bool)
|
|
}
|
|
|
|
func (c *Core) AuditLogger() AuditLogger {
|
|
return &basicAuditor{c: c}
|
|
}
|
|
|
|
type FeatureFlags struct {
|
|
NamespacesCubbyholesLocal bool `json:"namespace_cubbyholes_local"`
|
|
}
|
|
|
|
func (c *Core) persistFeatureFlags(ctx context.Context) error {
|
|
if !c.PR1103disabled {
|
|
c.logger.Debug("persisting feature flags")
|
|
json, err := jsonutil.EncodeJSON(&FeatureFlags{NamespacesCubbyholesLocal: !c.PR1103disabled})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.barrier.Put(ctx, &logical.StorageEntry{
|
|
Key: consts.CoreFeatureFlagPath,
|
|
Value: json,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Core) readFeatureFlags(ctx context.Context) (*FeatureFlags, error) {
|
|
entry, err := c.barrier.Get(ctx, consts.CoreFeatureFlagPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var flags FeatureFlags
|
|
if entry != nil {
|
|
err = jsonutil.DecodeJSON(entry.Value, &flags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &flags, nil
|
|
}
|
|
|
|
// isMountable tells us whether or not we can continue mounting a plugin-based
|
|
// mount entry after failing to instantiate a backend. We do this to preserve
|
|
// the storage and path when a plugin is missing or has otherwise been
|
|
// misconfigured. This allows users to recover from errors when starting Vault
|
|
// with misconfigured plugins. It should not be possible for existing builtins
|
|
// to be misconfigured, so that is a fatal error.
|
|
func (c *Core) isMountable(ctx context.Context, entry *MountEntry, pluginType consts.PluginType) bool {
|
|
// Prevent a panic early on
|
|
if entry == nil || c.pluginCatalog == nil {
|
|
return false
|
|
}
|
|
|
|
// Handle aliases
|
|
t := entry.Type
|
|
if alias, ok := mountAliases[t]; ok {
|
|
t = alias
|
|
}
|
|
|
|
plug, err := c.pluginCatalog.Get(ctx, t, pluginType, entry.Version)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return plug == nil || !plug.Builtin
|
|
}
|
|
|
|
// MatchingMount returns the path of the mount that will be responsible for
|
|
// handling the given request path.
|
|
func (c *Core) MatchingMount(ctx context.Context, reqPath string) string {
|
|
return c.router.MatchingMount(ctx, reqPath)
|
|
}
|
|
|
|
func (c *Core) setupQuotas(ctx context.Context, isPerfStandby bool) error {
|
|
if c.quotaManager == nil {
|
|
return nil
|
|
}
|
|
|
|
return c.quotaManager.Setup(ctx, c.systemBarrierView, isPerfStandby, c.IsDRSecondary())
|
|
}
|
|
|
|
// ApplyRateLimitQuota checks the request against all the applicable quota rules.
|
|
// If the given request's path is exempt, no rate limiting will be applied.
|
|
func (c *Core) ApplyRateLimitQuota(ctx context.Context, req *quotas.Request) (quotas.Response, error) {
|
|
req.Type = quotas.TypeRateLimit
|
|
|
|
resp := quotas.Response{
|
|
Allowed: true,
|
|
Headers: make(map[string]string),
|
|
}
|
|
|
|
if c.quotaManager != nil {
|
|
// skip rate limit checks for paths that are exempt from rate limiting
|
|
if c.quotaManager.RateLimitPathExempt(req.Path) {
|
|
return resp, nil
|
|
}
|
|
|
|
return c.quotaManager.ApplyQuota(ctx, req)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// RateLimitAuditLoggingEnabled returns if the quota configuration allows audit
|
|
// logging of request rejections due to rate limiting quota rule violations.
|
|
func (c *Core) RateLimitAuditLoggingEnabled() bool {
|
|
if c.quotaManager != nil {
|
|
return c.quotaManager.RateLimitAuditLoggingEnabled()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// RateLimitResponseHeadersEnabled returns if the quota configuration allows for
|
|
// rate limit quota HTTP headers to be added to responses.
|
|
func (c *Core) RateLimitResponseHeadersEnabled() bool {
|
|
if c.quotaManager != nil {
|
|
return c.quotaManager.RateLimitResponseHeadersEnabled()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *Core) KeyRotateGracePeriod() time.Duration {
|
|
return time.Duration(atomic.LoadInt64(c.keyRotateGracePeriod))
|
|
}
|
|
|
|
func (c *Core) SetKeyRotateGracePeriod(t time.Duration) {
|
|
atomic.StoreInt64(c.keyRotateGracePeriod, int64(t))
|
|
}
|
|
|
|
// Periodically test whether to automatically rotate the barrier key
|
|
func (c *Core) autoRotateBarrierLoop(ctx context.Context) {
|
|
t := time.NewTicker(autoRotateCheckInterval)
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
c.checkBarrierAutoRotate(ctx)
|
|
case <-ctx.Done():
|
|
t.Stop()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Core) checkBarrierAutoRotate(ctx context.Context) {
|
|
c.stateLock.RLock()
|
|
defer c.stateLock.RUnlock()
|
|
if c.isPrimary() {
|
|
reason, err := c.barrier.CheckBarrierAutoRotate(ctx)
|
|
if err != nil {
|
|
lf := c.logger.Error
|
|
if strings.HasSuffix(err.Error(), "context canceled") {
|
|
lf = c.logger.Debug
|
|
}
|
|
lf("error in barrier auto rotation", "error", err)
|
|
return
|
|
}
|
|
if reason != "" {
|
|
// Time to rotate. Invoke the rotation handler in order to both rotate and create
|
|
// the replication canary
|
|
c.logger.Info("automatic barrier key rotation triggered", "reason", reason)
|
|
|
|
_, err := c.systemBackend.handleRotate(ctx, nil, nil)
|
|
if err != nil {
|
|
c.logger.Error("error automatically rotating barrier key", "error", err)
|
|
} else {
|
|
metrics.IncrCounter(barrierRotationsMetric, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Core) isPrimary() bool {
|
|
return !c.ReplicationState().HasState(consts.ReplicationPerformanceSecondary | consts.ReplicationDRSecondary)
|
|
}
|
|
|
|
type LicenseState struct {
|
|
State string
|
|
ExpiryTime time.Time
|
|
Terminated bool
|
|
}
|
|
|
|
func (c *Core) loadLoginMFAConfigs(ctx context.Context) error {
|
|
eConfigs := make([]*mfa.MFAEnforcementConfig, 0)
|
|
allNamespaces := c.collectNamespaces()
|
|
for _, ns := range allNamespaces {
|
|
err := c.loginMFABackend.loadMFAMethodConfigs(ctx, ns)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading MFA method Config, namespaceid %s, error: %w", ns.ID, err)
|
|
}
|
|
|
|
loadedConfigs, err := c.loginMFABackend.loadMFAEnforcementConfigs(ctx, ns)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading MFA enforcement Config, namespaceid %s, error: %w", ns.ID, err)
|
|
}
|
|
|
|
eConfigs = append(eConfigs, loadedConfigs...)
|
|
}
|
|
|
|
for _, conf := range eConfigs {
|
|
if err := c.loginMFABackend.loginMFAMethodExistenceCheck(conf); err != nil {
|
|
c.loginMFABackend.mfaLogger.Error("failed to find all MFA methods that exist in MFA enforcement configs", "configID", conf.ID, "namespaceID", conf.NamespaceID, "error", err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type MFACachedAuthResponse struct {
|
|
CachedAuth *logical.Auth
|
|
RequestPath string
|
|
RequestNSID string
|
|
RequestNSPath string
|
|
RequestConnRemoteAddr string
|
|
TimeOfStorage time.Time
|
|
RequestID string
|
|
}
|
|
|
|
func (c *Core) setupCachedMFAResponseAuth() {
|
|
c.mfaResponseAuthQueueLock.Lock()
|
|
c.mfaResponseAuthQueue = NewLoginMFAPriorityQueue()
|
|
mfaQueue := c.mfaResponseAuthQueue
|
|
c.mfaResponseAuthQueueLock.Unlock()
|
|
|
|
ctx := c.activeContext
|
|
|
|
go func() {
|
|
ticker := time.Tick(5 * time.Second)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker:
|
|
err := mfaQueue.RemoveExpiredMfaAuthResponse(defaultMFAAuthResponseTTL, time.Now())
|
|
if err != nil {
|
|
c.Logger().Error("failed to remove stale MFA auth response", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return
|
|
}
|
|
|
|
// PopMFAResponseAuthByID pops an item from the mfaResponseAuthQueue by ID
|
|
// it returns the cached auth response or an error
|
|
func (c *Core) PopMFAResponseAuthByID(reqID string) (*MFACachedAuthResponse, error) {
|
|
c.mfaResponseAuthQueueLock.Lock()
|
|
defer c.mfaResponseAuthQueueLock.Unlock()
|
|
return c.mfaResponseAuthQueue.PopByKey(reqID)
|
|
}
|
|
|
|
// SaveMFAResponseAuth pushes an MFACachedAuthResponse to the mfaResponseAuthQueue.
|
|
// it returns an error in case of failure
|
|
func (c *Core) SaveMFAResponseAuth(respAuth *MFACachedAuthResponse) error {
|
|
c.mfaResponseAuthQueueLock.Lock()
|
|
defer c.mfaResponseAuthQueueLock.Unlock()
|
|
return c.mfaResponseAuthQueue.Push(respAuth)
|
|
}
|
|
|
|
type InFlightRequests struct {
|
|
InFlightReqMap *sync.Map
|
|
InFlightReqCount *uberAtomic.Uint64
|
|
}
|
|
|
|
type InFlightReqData struct {
|
|
StartTime time.Time `json:"start_time"`
|
|
ClientRemoteAddr string `json:"client_remote_address"`
|
|
ReqPath string `json:"request_path"`
|
|
Method string `json:"request_method"`
|
|
ClientID string `json:"client_id"`
|
|
}
|
|
|
|
func (c *Core) StoreInFlightReqData(reqID string, data InFlightReqData) {
|
|
c.inFlightReqData.InFlightReqMap.Store(reqID, data)
|
|
c.inFlightReqData.InFlightReqCount.Inc()
|
|
}
|
|
|
|
// FinalizeInFlightReqData is going log the completed request if the
|
|
// corresponding server config option is enabled. It also removes the
|
|
// request from the inFlightReqMap and decrement the number of in-flight
|
|
// requests by one.
|
|
func (c *Core) FinalizeInFlightReqData(reqID string, statusCode int) {
|
|
if c.logRequestsLevel != nil && c.logRequestsLevel.Load() != 0 {
|
|
c.LogCompletedRequests(reqID, statusCode)
|
|
}
|
|
|
|
c.inFlightReqData.InFlightReqMap.Delete(reqID)
|
|
c.inFlightReqData.InFlightReqCount.Dec()
|
|
}
|
|
|
|
// LoadInFlightReqData creates a snapshot map of the current
|
|
// in-flight requests
|
|
func (c *Core) LoadInFlightReqData() map[string]InFlightReqData {
|
|
currentInFlightReqMap := make(map[string]InFlightReqData)
|
|
c.inFlightReqData.InFlightReqMap.Range(func(key, value interface{}) bool {
|
|
// there is only one writer to this map, so skip checking for errors
|
|
v := value.(InFlightReqData)
|
|
currentInFlightReqMap[key.(string)] = v
|
|
return true
|
|
})
|
|
|
|
return currentInFlightReqMap
|
|
}
|
|
|
|
// UpdateInFlightReqData updates the data for a specific reqID with
|
|
// the clientID
|
|
func (c *Core) UpdateInFlightReqData(reqID, clientID string) {
|
|
v, ok := c.inFlightReqData.InFlightReqMap.Load(reqID)
|
|
if !ok {
|
|
c.Logger().Trace("failed to retrieve request with ID", "request_id", reqID)
|
|
return
|
|
}
|
|
|
|
// there is only one writer to this map, so skip checking for errors
|
|
reqData := v.(InFlightReqData)
|
|
reqData.ClientID = clientID
|
|
c.inFlightReqData.InFlightReqMap.Store(reqID, reqData)
|
|
}
|
|
|
|
// LogCompletedRequests Logs the completed request to the server logs
|
|
func (c *Core) LogCompletedRequests(reqID string, statusCode int) {
|
|
logLevel := log.Level(c.logRequestsLevel.Load())
|
|
v, ok := c.inFlightReqData.InFlightReqMap.Load(reqID)
|
|
if !ok {
|
|
c.logger.Log(logLevel, fmt.Sprintf("failed to retrieve request with ID %v", reqID))
|
|
return
|
|
}
|
|
|
|
// there is only one writer to this map, so skip checking for errors
|
|
reqData := v.(InFlightReqData)
|
|
c.logger.Log(logLevel, "completed_request",
|
|
"start_time", reqData.StartTime.Format(time.RFC3339),
|
|
"duration", fmt.Sprintf("%dms", time.Now().Sub(reqData.StartTime).Milliseconds()),
|
|
"client_id", reqData.ClientID,
|
|
"client_address", reqData.ClientRemoteAddr, "status_code", statusCode, "request_path", reqData.ReqPath,
|
|
"request_method", reqData.Method)
|
|
}
|
|
|
|
func (c *Core) ReloadLogRequestsLevel() {
|
|
conf := c.rawConfig.Load()
|
|
if conf == nil {
|
|
return
|
|
}
|
|
|
|
infoLevel := conf.(*server.Config).LogRequestsLevel
|
|
switch {
|
|
case log.LevelFromString(infoLevel) > log.NoLevel && log.LevelFromString(infoLevel) < log.Off:
|
|
c.logRequestsLevel.Store(int32(log.LevelFromString(infoLevel)))
|
|
case infoLevel != "":
|
|
c.logger.Warn("invalid log_requests_level", "level", infoLevel)
|
|
}
|
|
}
|
|
|
|
type PeerNode struct {
|
|
Hostname string `json:"hostname"`
|
|
APIAddress string `json:"api_address"`
|
|
ClusterAddress string `json:"cluster_address"`
|
|
Version string `json:"version"`
|
|
LastEcho time.Time `json:"last_echo"`
|
|
UpgradeVersion string `json:"upgrade_version,omitempty"`
|
|
RedundancyZone string `json:"redundancy_zone,omitempty"`
|
|
}
|
|
|
|
// GetHAPeerNodesCached returns the nodes that've sent us Echo requests recently.
|
|
func (c *Core) GetHAPeerNodesCached() []PeerNode {
|
|
var nodes []PeerNode
|
|
for itemClusterAddr, item := range c.clusterPeerClusterAddrsCache.Items() {
|
|
info := item.Object.(nodeHAConnectionInfo)
|
|
nodes = append(nodes, PeerNode{
|
|
Hostname: info.nodeInfo.Hostname,
|
|
APIAddress: info.nodeInfo.ApiAddr,
|
|
ClusterAddress: itemClusterAddr,
|
|
LastEcho: info.lastHeartbeat,
|
|
Version: info.version,
|
|
UpgradeVersion: info.upgradeVersion,
|
|
RedundancyZone: info.redundancyZone,
|
|
})
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
func (c *Core) CheckPluginPerms(pluginName string) (err error) {
|
|
var enableFilePermissionsCheck bool
|
|
if enableFilePermissionsCheckEnv := os.Getenv(consts.VaultEnableFilePermissionsCheckEnv); enableFilePermissionsCheckEnv != "" {
|
|
var err error
|
|
enableFilePermissionsCheck, err = strconv.ParseBool(enableFilePermissionsCheckEnv)
|
|
if err != nil {
|
|
return errors.New("Error parsing the environment variable VAULT_ENABLE_FILE_PERMISSIONS_CHECK")
|
|
}
|
|
}
|
|
|
|
if c.pluginDirectory != "" && enableFilePermissionsCheck {
|
|
err = osutil.OwnerPermissionsMatch(c.pluginDirectory, c.pluginFileUid, c.pluginFilePermissions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fullPath := filepath.Join(c.pluginDirectory, pluginName)
|
|
err = osutil.OwnerPermissionsMatch(fullPath, c.pluginFileUid, c.pluginFilePermissions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (c *Core) LoadNodeID() (string, error) {
|
|
raftNodeID := c.GetRaftNodeID()
|
|
if raftNodeID != "" {
|
|
return raftNodeID, nil
|
|
}
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return hostname, nil
|
|
}
|
|
|
|
// DetermineRoleFromLoginRequestFromBytes will determine the role that should be applied to a quota for a given
|
|
// login request, accepting a byte payload
|
|
func (c *Core) DetermineRoleFromLoginRequestFromBytes(mountPoint string, payload []byte, ctx context.Context) string {
|
|
data := make(map[string]interface{})
|
|
err := jsonutil.DecodeJSON(payload, &data)
|
|
if err != nil {
|
|
// Cannot discern a role from a request we cannot parse
|
|
return ""
|
|
}
|
|
|
|
return c.DetermineRoleFromLoginRequest(mountPoint, data, ctx)
|
|
}
|
|
|
|
// DetermineRoleFromLoginRequest will determine the role that should be applied to a quota for a given
|
|
// login request
|
|
func (c *Core) DetermineRoleFromLoginRequest(mountPoint string, data map[string]interface{}, ctx context.Context) string {
|
|
c.authLock.RLock()
|
|
defer c.authLock.RUnlock()
|
|
matchingBackend := c.router.MatchingBackend(ctx, mountPoint)
|
|
if matchingBackend == nil || matchingBackend.Type() != logical.TypeCredential {
|
|
// Role based quotas do not apply to this request
|
|
return ""
|
|
}
|
|
|
|
resp, err := matchingBackend.HandleRequest(ctx, &logical.Request{
|
|
MountPoint: mountPoint,
|
|
Path: "login",
|
|
Operation: logical.ResolveRoleOperation,
|
|
Data: data,
|
|
Storage: c.router.MatchingStorageByAPIPath(ctx, mountPoint+"login"),
|
|
})
|
|
if err != nil || resp.Data["role"] == nil {
|
|
return ""
|
|
}
|
|
return resp.Data["role"].(string)
|
|
}
|
|
|
|
// aliasNameFromLoginRequest will determine the aliasName from the login Request
|
|
func (c *Core) aliasNameFromLoginRequest(ctx context.Context, req *logical.Request) (string, error) {
|
|
c.authLock.RLock()
|
|
defer c.authLock.RUnlock()
|
|
ns, err := namespace.FromContext(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// ns path is added while checking matching backend
|
|
mountPath := strings.TrimPrefix(req.MountPoint, ns.Path)
|
|
|
|
matchingBackend := c.router.MatchingBackend(ctx, mountPath)
|
|
if matchingBackend == nil || matchingBackend.Type() != logical.TypeCredential {
|
|
// pathLoginAliasLookAhead operation does not apply to this request
|
|
return "", nil
|
|
}
|
|
|
|
path := strings.ReplaceAll(req.Path, mountPath, "")
|
|
|
|
resp, err := matchingBackend.HandleRequest(ctx, &logical.Request{
|
|
MountPoint: req.MountPoint,
|
|
Path: path,
|
|
Operation: logical.AliasLookaheadOperation,
|
|
Data: req.Data,
|
|
Storage: c.router.MatchingStorageByAPIPath(ctx, req.Path),
|
|
})
|
|
if err != nil || resp.Auth.Alias == nil {
|
|
return "", nil
|
|
}
|
|
return resp.Auth.Alias.Name, nil
|
|
}
|
|
|
|
// ListMounts will provide a slice containing a deep copy each mount entry
|
|
func (c *Core) ListMounts() ([]*MountEntry, error) {
|
|
c.mountsLock.RLock()
|
|
defer c.mountsLock.RUnlock()
|
|
|
|
var entries []*MountEntry
|
|
|
|
for _, entry := range c.mounts.Entries {
|
|
clone, err := entry.Clone()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries = append(entries, clone)
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// ListAuths will provide a slice containing a deep copy each auth entry
|
|
func (c *Core) ListAuths() ([]*MountEntry, error) {
|
|
c.mountsLock.RLock()
|
|
defer c.mountsLock.RUnlock()
|
|
|
|
var entries []*MountEntry
|
|
|
|
for _, entry := range c.auth.Entries {
|
|
clone, err := entry.Clone()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries = append(entries, clone)
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
type HCPLinkStatus struct {
|
|
lock sync.RWMutex
|
|
ConnectionStatus string `json:"hcp_link_status,omitempty"`
|
|
ResourceIDOnHCP string `json:"resource_ID_on_hcp,omitempty"`
|
|
}
|
|
|
|
func (c *Core) SetHCPLinkStatus(status, resourceID string) {
|
|
c.hcpLinkStatus.lock.Lock()
|
|
defer c.hcpLinkStatus.lock.Unlock()
|
|
c.hcpLinkStatus.ConnectionStatus = status
|
|
c.hcpLinkStatus.ResourceIDOnHCP = resourceID
|
|
}
|
|
|
|
func (c *Core) GetHCPLinkStatus() (string, string) {
|
|
c.hcpLinkStatus.lock.RLock()
|
|
defer c.hcpLinkStatus.lock.RUnlock()
|
|
|
|
status := c.hcpLinkStatus.ConnectionStatus
|
|
resourceID := c.hcpLinkStatus.ResourceIDOnHCP
|
|
|
|
return status, resourceID
|
|
}
|