2022-09-06 07:50:15 -07:00
// Copyright (c) HashiCorp, Inc.
2021-06-22 22:26:26 -04:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2021-06-22 22:26:26 -04:00
package vault
import (
"context"
"net/http"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
2023-09-12 08:15:34 -04:00
"github.com/hashicorp/boundary/internal/event"
2021-06-22 22:26:26 -04:00
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/scheduler"
vault "github.com/hashicorp/vault/api"
ua "go.uber.org/atomic"
)
const (
tokenRenewalJobName = "vault_token_renewal"
tokenRevocationJobName = "vault_token_revocation"
credentialRenewalJobName = "vault_credential_renewal"
credentialRevocationJobName = "vault_credential_revocation"
credentialStoreCleanupJobName = "vault_credential_store_cleanup"
credentialCleanupJobName = "vault_credential_cleanup"
defaultNextRunIn = 5 * time . Minute
renewalWindow = 10 * time . Minute
)
2021-08-01 14:41:13 -04:00
func RegisterJobs ( ctx context . Context , scheduler * scheduler . Scheduler , r db . Reader , w db . Writer , kms * kms . Kms ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.RegisterJobs"
2023-06-06 17:09:08 -04:00
tokenRenewal , err := newTokenRenewalJob ( ctx , r , w , kms )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err = scheduler . RegisterJob ( ctx , tokenRenewal ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "token renewal job" ))
2021-06-22 22:26:26 -04:00
}
2023-06-06 17:09:08 -04:00
tokenRevoke , err := newTokenRevocationJob ( ctx , r , w , kms )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err = scheduler . RegisterJob ( ctx , tokenRevoke ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "token revocation job" ))
2021-06-22 22:26:26 -04:00
}
2023-06-06 17:09:08 -04:00
credRenewal , err := newCredentialRenewalJob ( ctx , r , w , kms )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err = scheduler . RegisterJob ( ctx , credRenewal ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "credential renewal job" ))
2021-06-22 22:26:26 -04:00
}
2023-06-06 17:09:08 -04:00
credRevoke , err := newCredentialRevocationJob ( ctx , r , w , kms )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err = scheduler . RegisterJob ( ctx , credRevoke ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "credential revocation job" ))
2021-06-22 22:26:26 -04:00
}
2023-06-06 17:09:08 -04:00
credStoreCleanup , err := newCredentialStoreCleanupJob ( ctx , r , w , kms )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err = scheduler . RegisterJob ( ctx , credStoreCleanup ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "credential store cleanup job" ))
2021-06-22 22:26:26 -04:00
}
2023-06-06 17:09:08 -04:00
credCleanup , err := newCredentialCleanupJob ( ctx , w )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err = scheduler . RegisterJob ( ctx , credCleanup ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "credential cleanup job" ))
2021-06-22 22:26:26 -04:00
}
return nil
}
// TokenRenewalJob is the recurring job that renews credential store Vault tokens that
// are in the `current` and `maintaining` state. The TokenRenewalJob is not thread safe,
// an attempt to Run the job concurrently will result in an JobAlreadyRunning error.
type TokenRenewalJob struct {
reader db . Reader
writer db . Writer
kms * kms . Kms
limit int
running ua . Bool
numTokens int
numProcessed int
}
// newTokenRenewalJob creates a new in-memory TokenRenewalJob.
//
// WithLimit is the only supported option.
2023-06-06 17:09:08 -04:00
func newTokenRenewalJob ( ctx context . Context , r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * TokenRenewalJob , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.newTokenRenewalJob"
switch {
case r == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Reader" )
2021-06-22 22:26:26 -04:00
case w == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Writer" )
2021-06-22 22:26:26 -04:00
case kms == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing kms" )
2021-06-22 22:26:26 -04:00
}
opts := getOpts ( opt ... )
if opts . withLimit == 0 {
// zero signals the boundary defaults should be used.
opts . withLimit = db . DefaultLimit
}
return & TokenRenewalJob {
reader : r ,
writer : w ,
kms : kms ,
limit : opts . withLimit ,
}, nil
}
// Status returns the current status of the token renewal job. Total is the total number
// of tokens that are set to be renewed. Completed is the number of tokens already renewed.
func ( r * TokenRenewalJob ) Status () scheduler . JobStatus {
return scheduler . JobStatus {
Completed : r . numProcessed ,
Total : r . numTokens ,
}
}
// Run queries the vault credential repo for tokens that need to be renewed, it then creates
// a vault client and renews each token. Can not be run in parallel, if Run is invoked while
// already running an error with code JobAlreadyRunning will be returned.
2024-09-20 12:14:29 -04:00
func ( r * TokenRenewalJob ) Run ( ctx context . Context , _ time . Duration ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(TokenRenewalJob).Run"
2024-02-13 10:54:48 -05:00
if ! r . running . CompareAndSwap ( r . running . Load (), true ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . JobAlreadyRunning , op , "job already running" )
2021-06-22 22:26:26 -04:00
}
defer r . running . Store ( false )
// Verify context is not done before running
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2022-09-06 07:50:15 -07:00
var ps [] * renewRevokeStore
2021-06-22 22:26:26 -04:00
// Fetch all tokens that will reach their renewal point within the renewalWindow.
// This is done to avoid constantly scheduling the token renewal job when there are multiple tokens
// set to renew in sequence.
2022-10-25 18:29:38 -05:00
err := r . reader . SearchWhere ( ctx , & ps , `token_renewal_time < wt_add_seconds_to_now(?)` , [] any { renewalWindow . Seconds ()}, db . WithLimit ( r . limit ))
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// Set numProcessed and numTokens for status report
r . numProcessed , r . numTokens = 0 , len ( ps )
2022-09-06 07:50:15 -07:00
for _ , as := range ps {
s := as . Store
2021-06-22 22:26:26 -04:00
// Verify context is not done before renewing next token
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err := r . renewToken ( ctx , s ); err != nil {
2022-09-06 07:50:15 -07:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error renewing token" , "credential store id" , s . PublicId , "token status" , s . TokenStatus ))
2021-06-22 22:26:26 -04:00
}
r . numProcessed ++
}
return nil
}
2022-09-06 07:50:15 -07:00
func ( r * TokenRenewalJob ) renewToken ( ctx context . Context , s * clientStore ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(TokenRenewalJob).renewToken"
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , s . ProjectId , kms . KeyPurposeDatabase )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2021-06-22 22:26:26 -04:00
}
if err = s . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
token := s . token ()
if token == nil {
// Store has no token to renew
return nil
}
2022-08-25 14:37:22 -04:00
vc , err := s . client ( ctx )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
var respErr * vault . ResponseError
2022-08-25 14:37:22 -04:00
renewedToken , err := vc . renewToken ( ctx )
2021-06-22 22:26:26 -04:00
if ok := errors . As ( err , & respErr ); ok && respErr . StatusCode == http . StatusForbidden {
// Vault returned a 403 when attempting a renew self, the token is either expired
// or malformed. Set status to "expired" so credentials created with token can be
// cleaned up.
query , values := token . updateStatusQuery ( ExpiredToken )
numRows , err := r . writer . Exec ( ctx , query , values )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if numRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Unknown , op , "token expired but failed to update repo" )
2021-06-22 22:26:26 -04:00
}
if s . TokenStatus == string ( CurrentToken ) {
2022-09-06 07:50:15 -07:00
event . WriteSysEvent ( ctx , op , "Vault credential store current token has expired" , "credential store id" , s . PublicId )
2021-06-22 22:26:26 -04:00
}
// Set credentials associated with this token to expired as Vault will already cascade delete them
2022-10-25 18:29:38 -05:00
_ , err = r . writer . Exec ( ctx , updateCredentialStatusByTokenQuery , [] any { ExpiredCredential , token . TokenHmac })
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "error updating credentials to revoked after revoking token" ))
2021-06-22 22:26:26 -04:00
}
return nil
}
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to renew vault token" ))
2021-06-22 22:26:26 -04:00
}
tokenExpires , err := renewedToken . TokenTTL ()
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get vault token expiration" ))
2021-06-22 22:26:26 -04:00
}
token . expiration = tokenExpires
query , values := token . updateExpirationQuery ()
numRows , err := r . writer . Exec ( ctx , query , values )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if numRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Unknown , op , "token renewed but failed to update repo" )
2021-06-22 22:26:26 -04:00
}
return nil
}
// NextRunIn queries the vault credential repo to determine when the next token renewal job should run.
2022-01-05 14:38:38 -05:00
func ( r * TokenRenewalJob ) NextRunIn ( ctx context . Context ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.(TokenRenewalJob).NextRunIn"
2022-01-05 14:38:38 -05:00
next , err := nextRenewal ( ctx , r )
2021-06-22 22:26:26 -04:00
if err != nil {
2023-06-06 17:09:08 -04:00
return defaultNextRunIn , errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
return next , nil
}
2022-01-05 14:38:38 -05:00
func nextRenewal ( ctx context . Context , j scheduler . Job ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.nextRenewal"
var query string
var r db . Reader
switch job := j .( type ) {
case * TokenRenewalJob :
query = tokenRenewalNextRunInQuery
r = job . reader
case * CredentialRenewalJob :
query = credentialRenewalNextRunInQuery
r = job . reader
default :
2023-06-06 17:09:08 -04:00
return 0 , errors . New ( ctx , errors . Unknown , op , "unknown job" )
2021-06-22 22:26:26 -04:00
}
rows , err := r . Query ( context . Background (), query , nil )
if err != nil {
2023-06-06 17:09:08 -04:00
return 0 , errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
defer rows . Close ()
2024-02-13 10:54:48 -05:00
if rows . Next () {
2021-06-22 22:26:26 -04:00
type NextRenewal struct {
RenewalIn time . Duration
}
var n NextRenewal
2022-01-05 14:38:38 -05:00
err = r . ScanRows ( ctx , rows , & n )
2021-06-22 22:26:26 -04:00
if err != nil {
2023-06-06 17:09:08 -04:00
return 0 , errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if n . RenewalIn < 0 {
// If we are past the next renewal time, return 0 to schedule immediately
return 0 , nil
}
return n . RenewalIn * time . Second , nil
}
2024-01-22 14:03:40 -05:00
if err := rows . Err (); err != nil {
return 0 , errors . Wrap ( ctx , err , op )
}
2021-06-22 22:26:26 -04:00
return defaultNextRunIn , nil
}
// Name is the unique name of the job.
func ( r * TokenRenewalJob ) Name () string {
return tokenRenewalJobName
}
// Description is the human readable description of the job.
func ( r * TokenRenewalJob ) Description () string {
return "Periodically renews Vault credential store tokens that are in a maintaining or current state."
}
// TokenRevocationJob is the recurring job that revokes credential store Vault tokens that
// are in the `maintaining` state and have no credentials being used by an active or pending session.
// The TokenRevocationJob is not thread safe, an attempt to Run the job concurrently will result in
// an JobAlreadyRunning error.
type TokenRevocationJob struct {
reader db . Reader
writer db . Writer
kms * kms . Kms
limit int
running ua . Bool
numTokens int
numProcessed int
}
// newTokenRevocationJob creates a new in-memory TokenRevocationJob.
//
// WithLimit is the only supported option.
2023-06-06 17:09:08 -04:00
func newTokenRevocationJob ( ctx context . Context , r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * TokenRevocationJob , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.newTokenRevocationJob"
switch {
case r == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Reader" )
2021-06-22 22:26:26 -04:00
case w == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Writer" )
2021-06-22 22:26:26 -04:00
case kms == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing kms" )
2021-06-22 22:26:26 -04:00
}
opts := getOpts ( opt ... )
if opts . withLimit == 0 {
// zero signals the boundary defaults should be used.
opts . withLimit = db . DefaultLimit
}
return & TokenRevocationJob {
reader : r ,
writer : w ,
kms : kms ,
limit : opts . withLimit ,
}, nil
}
// Status returns the current status of the token revocation job. Total is the total number
// of tokens that are set to be revoked. Completed is the number of tokens already revoked.
func ( r * TokenRevocationJob ) Status () scheduler . JobStatus {
return scheduler . JobStatus {
Completed : r . numProcessed ,
Total : r . numTokens ,
}
}
// Run queries the vault credential repo for tokens that need to be revoked, it then creates
// a vault client and revokes each token. Can not be run in parallel, if Run is invoked while
// already running an error with code JobAlreadyRunning will be returned.
2024-09-20 12:14:29 -04:00
func ( r * TokenRevocationJob ) Run ( ctx context . Context , _ time . Duration ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(TokenRevocationJob).Run"
2024-02-13 10:54:48 -05:00
if ! r . running . CompareAndSwap ( r . running . Load (), true ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . JobAlreadyRunning , op , "job already running" )
2021-06-22 22:26:26 -04:00
}
defer r . running . Store ( false )
// Verify context is not done before running
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// Fetch all tokens in the revoke state as well as all tokens in the maintaining state
// that have no credentials in an active state.
where := `
token_status = 'revoke'
or
(token_status = 'maintaining'
and token_hmac not in (
select token_hmac from credential_vault_credential
where status = 'active'
))
`
2022-09-06 07:50:15 -07:00
var ps [] * renewRevokeStore
2021-06-22 22:26:26 -04:00
err := r . reader . SearchWhere ( ctx , & ps , where , nil , db . WithLimit ( r . limit ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// Set numProcessed and numTokens for s report
r . numProcessed , r . numTokens = 0 , len ( ps )
2022-09-06 07:50:15 -07:00
for _ , as := range ps {
s := as . Store
2021-06-22 22:26:26 -04:00
// Verify context is not done before renewing next token
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if err := r . revokeToken ( ctx , s ); err != nil {
2022-09-06 07:50:15 -07:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error revoking token" , "credential store id" , s . PublicId ))
2021-06-22 22:26:26 -04:00
}
r . numProcessed ++
}
return nil
}
2022-09-06 07:50:15 -07:00
func ( r * TokenRevocationJob ) revokeToken ( ctx context . Context , s * clientStore ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(TokenRevocationJob).revokeToken"
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , s . ProjectId , kms . KeyPurposeDatabase )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2021-06-22 22:26:26 -04:00
}
if err = s . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
token := s . token ()
if token == nil {
// Store has no token to revoke
return nil
}
2022-08-25 14:37:22 -04:00
vc , err := s . client ( ctx )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
var respErr * vault . ResponseError
2022-08-25 14:37:22 -04:00
err = vc . revokeToken ( ctx )
2021-06-22 22:26:26 -04:00
if ok := errors . As ( err , & respErr ); ok && respErr . StatusCode == http . StatusForbidden {
// Vault returned a 403 when attempting a revoke self, the token is already expired.
// Clobber error and set status to "revoked" below.
err = nil
}
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to revoke vault token" ))
2021-06-22 22:26:26 -04:00
}
query , values := token . updateStatusQuery ( RevokedToken )
numRows , err := r . writer . Exec ( ctx , query , values )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if numRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Unknown , op , "token revoked but failed to update repo" )
2021-06-22 22:26:26 -04:00
}
// Set credentials associated with this token to revoked as Vault will already cascade revoke them
2022-10-25 18:29:38 -05:00
_ , err = r . writer . Exec ( ctx , updateCredentialStatusByTokenQuery , [] any { RevokedCredential , token . TokenHmac })
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "error updating credentials to revoked after revoking token" ))
2021-06-22 22:26:26 -04:00
}
return nil
}
// NextRunIn determines when the next token revocation job should run.
2022-01-05 14:38:38 -05:00
func ( r * TokenRevocationJob ) NextRunIn ( _ context . Context ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
return defaultNextRunIn , nil
}
// Name is the unique name of the job.
func ( r * TokenRevocationJob ) Name () string {
return tokenRevocationJobName
}
// Description is the human readable description of the job.
func ( r * TokenRevocationJob ) Description () string {
return "Periodically revokes Vault credential store tokens that are in a maintaining state and have no active credentials associated."
}
// CredentialRenewalJob is the recurring job that renews Vault credentials issued to a session.
// The CredentialRenewalJob is not thread safe, an attempt to Run the job concurrently will result
// in an JobAlreadyRunning error.
type CredentialRenewalJob struct {
reader db . Reader
writer db . Writer
kms * kms . Kms
limit int
running ua . Bool
numCreds int
numProcessed int
}
// newCredentialRenewalJob creates a new in-memory CredentialRenewalJob.
//
// WithLimit is the only supported option.
2023-06-06 17:09:08 -04:00
func newCredentialRenewalJob ( ctx context . Context , r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * CredentialRenewalJob , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.newCredentialRenewalJob"
switch {
case r == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Reader" )
2021-06-22 22:26:26 -04:00
case w == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Writer" )
2021-06-22 22:26:26 -04:00
case kms == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing kms" )
2021-06-22 22:26:26 -04:00
}
opts := getOpts ( opt ... )
if opts . withLimit == 0 {
// zero signals the boundary defaults should be used.
opts . withLimit = db . DefaultLimit
}
return & CredentialRenewalJob {
reader : r ,
writer : w ,
kms : kms ,
limit : opts . withLimit ,
}, nil
}
// Status returns the current status of the credential renewal job. Total is the total number
// of credentials that are set to be renewed. Completed is the number of credential already renewed.
func ( r * CredentialRenewalJob ) Status () scheduler . JobStatus {
return scheduler . JobStatus {
Completed : r . numProcessed ,
Total : r . numCreds ,
}
}
// Run queries the vault credential repo for credentials that need to be renewed, it then creates
// a vault client and renews each credential. Can not be run in parallel, if Run is invoked while
// already running an error with code JobAlreadyRunning will be returned.
2024-09-20 12:14:29 -04:00
func ( r * CredentialRenewalJob ) Run ( ctx context . Context , _ time . Duration ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(CredentialRenewalJob).Run"
2024-02-13 10:54:48 -05:00
if ! r . running . CompareAndSwap ( r . running . Load (), true ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . JobAlreadyRunning , op , "job already running" )
2021-06-22 22:26:26 -04:00
}
defer r . running . Store ( false )
// Verify context is not done before running
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
var creds [] * privateCredential
// Fetch all active credentials that will reach their renewal point within the renewalWindow.
// This is done to avoid constantly scheduling the credential renewal job when there are
// multiple credentials set to renew in sequence.
2022-10-25 18:29:38 -05:00
err := r . reader . SearchWhere ( ctx , & creds , `renewal_time < wt_add_seconds_to_now(?) and status = ?` , [] any { renewalWindow . Seconds (), ActiveCredential }, db . WithLimit ( r . limit ))
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// Set numProcessed and numTokens for status report
r . numProcessed , r . numCreds = 0 , len ( creds )
for _ , c := range creds {
// Verify context is not done before renewing next token
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2024-04-06 13:48:47 -07:00
if c . SessionCorrelationId != "" {
ctx , err = event . NewCorrelationIdContext ( ctx , c . SessionCorrelationId )
if err != nil {
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error generating correlation context" , "credential id" , c . PublicId , "session id" , c . SessionId ))
continue
}
}
2021-06-22 22:26:26 -04:00
if err := r . renewCred ( ctx , c ); err != nil {
2021-07-28 06:31:16 -04:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error renewing credential" , "credential id" , c . PublicId ))
2021-06-22 22:26:26 -04:00
}
r . numProcessed ++
}
return nil
}
func ( r * CredentialRenewalJob ) renewCred ( ctx context . Context , c * privateCredential ) error {
const op = "vault.(CredentialRenewalJob).renewCred"
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeDatabase )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2021-06-22 22:26:26 -04:00
}
if err = c . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2022-08-25 14:37:22 -04:00
vc , err := c . client ( ctx )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
cred := c . toCredential ()
var respErr * vault . ResponseError
// Subtract last renewal time from previous expiration time to get lease duration
leaseDuration := c . ExpirationTime . AsTime (). Sub ( c . LastRenewalTime . AsTime ())
2022-08-25 14:37:22 -04:00
renewedCred , err := vc . renewLease ( ctx , c . ExternalId , leaseDuration )
2021-06-22 22:26:26 -04:00
if ok := errors . As ( err , & respErr ); ok && respErr . StatusCode == http . StatusBadRequest {
// Vault returned a 400 when attempting a renew lease, the lease is either expired
// or the leaseId is malformed. Set status to "expired".
query , values := cred . updateStatusQuery ( ExpiredCredential )
numRows , err := r . writer . Exec ( ctx , query , values )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if numRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Unknown , op , "credential expired but failed to update repo" )
2021-06-22 22:26:26 -04:00
}
return nil
}
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to renew credential" ))
2021-06-22 22:26:26 -04:00
}
2024-09-18 17:46:12 +01:00
if renewedCred == nil {
return errors . New ( ctx , errors . Unknown , op , "vault returned empty credential" )
}
2021-06-22 22:26:26 -04:00
cred . expiration = time . Duration ( renewedCred . LeaseDuration ) * time . Second
query , values := cred . updateExpirationQuery ()
numRows , err := r . writer . Exec ( ctx , query , values )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if numRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Unknown , op , "credential renewed but failed to update repo" )
2021-06-22 22:26:26 -04:00
}
return nil
}
// NextRunIn queries the vault credential repo to determine when the next credential renewal job should run.
2022-01-05 14:38:38 -05:00
func ( r * CredentialRenewalJob ) NextRunIn ( ctx context . Context ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.(CredentialRenewalJob).NextRunIn"
2022-01-05 14:38:38 -05:00
next , err := nextRenewal ( ctx , r )
2021-06-22 22:26:26 -04:00
if err != nil {
2023-06-06 17:09:08 -04:00
return defaultNextRunIn , errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
return next , nil
}
// Name is the unique name of the job.
func ( r * CredentialRenewalJob ) Name () string {
return credentialRenewalJobName
}
// Description is the human readable description of the job.
func ( r * CredentialRenewalJob ) Description () string {
return "Periodically renews Vault credentials that are attached to an active/pending session (in the active state)."
}
// CredentialRevocationJob is the recurring job that revokes Vault credentials that are no
// longer being used by an active or pending session.
// The CredentialRevocationJob is not thread safe, an attempt to Run the job concurrently
// will result in an JobAlreadyRunning error.
type CredentialRevocationJob struct {
reader db . Reader
writer db . Writer
kms * kms . Kms
limit int
running ua . Bool
numCreds int
numProcessed int
}
// newCredentialRevocationJob creates a new in-memory CredentialRevocationJob.
//
// WithLimit is the only supported option.
2023-06-06 17:09:08 -04:00
func newCredentialRevocationJob ( ctx context . Context , r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * CredentialRevocationJob , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.newCredentialRevocationJob"
switch {
case r == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Reader" )
2021-06-22 22:26:26 -04:00
case w == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Writer" )
2021-06-22 22:26:26 -04:00
case kms == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing kms" )
2021-06-22 22:26:26 -04:00
}
opts := getOpts ( opt ... )
if opts . withLimit == 0 {
// zero signals the boundary defaults should be used.
opts . withLimit = db . DefaultLimit
}
return & CredentialRevocationJob {
reader : r ,
writer : w ,
kms : kms ,
limit : opts . withLimit ,
}, nil
}
// Status returns the current status of the credential revocation job. Total is the total number
// of credentials that are set to be revoked. Completed is the number of credentials already revoked.
func ( r * CredentialRevocationJob ) Status () scheduler . JobStatus {
return scheduler . JobStatus {
Completed : r . numProcessed ,
Total : r . numCreds ,
}
}
// Run queries the vault credential repo for credentials that need to be revoked, it then creates
// a vault client and revokes each credential. Can not be run in parallel, if Run is invoked while
// already running an error with code JobAlreadyRunning will be returned.
2024-09-20 12:14:29 -04:00
func ( r * CredentialRevocationJob ) Run ( ctx context . Context , _ time . Duration ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(CredentialRevocationJob).Run"
2024-02-13 10:54:48 -05:00
if ! r . running . CompareAndSwap ( r . running . Load (), true ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . JobAlreadyRunning , op , "job already running" )
2021-06-22 22:26:26 -04:00
}
defer r . running . Store ( false )
// Verify context is not done before running
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
var creds [] * privateCredential
2022-10-25 18:29:38 -05:00
err := r . reader . SearchWhere ( ctx , & creds , "status = ?" , [] any { RevokeCredential }, db . WithLimit ( r . limit ))
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// Set numProcessed and numTokens for status report
r . numProcessed , r . numCreds = 0 , len ( creds )
for _ , c := range creds {
// Verify context is not done before renewing next token
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2024-04-06 13:48:47 -07:00
if c . SessionCorrelationId != "" {
ctx , err = event . NewCorrelationIdContext ( ctx , c . SessionCorrelationId )
if err != nil {
// log the error, but we should still revoke the credential in Vault since it is
// no longer being used.
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error generating correlation context" , "credential id" , c . PublicId , "session id" , c . SessionId ))
}
}
2021-06-22 22:26:26 -04:00
if err := r . revokeCred ( ctx , c ); err != nil {
2021-07-28 06:31:16 -04:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error revoking credential" , "credential id" , c . PublicId ))
2021-06-22 22:26:26 -04:00
}
r . numProcessed ++
}
return nil
}
func ( r * CredentialRevocationJob ) revokeCred ( ctx context . Context , c * privateCredential ) error {
const op = "vault.(CredentialRenewalJob).revokeCred"
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , c . ProjectId , kms . KeyPurposeDatabase )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2021-06-22 22:26:26 -04:00
}
if err = c . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2022-08-25 14:37:22 -04:00
vc , err := c . client ( ctx )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
cred := c . toCredential ()
var respErr * vault . ResponseError
2022-08-25 14:37:22 -04:00
err = vc . revokeLease ( ctx , c . ExternalId )
2021-06-22 22:26:26 -04:00
if ok := errors . As ( err , & respErr ); ok && respErr . StatusCode == http . StatusBadRequest {
// Vault returned a 400 when attempting a revoke lease, the lease is already expired.
// Clobber error and set status to "revoked" below.
err = nil
}
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to revoke credential" ))
2021-06-22 22:26:26 -04:00
}
query , values := cred . updateStatusQuery ( RevokedCredential )
numRows , err := r . writer . Exec ( ctx , query , values )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
if numRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . Unknown , op , "credential revoked but failed to update repo" )
2021-06-22 22:26:26 -04:00
}
return nil
}
// NextRunIn determine when the next credential revocation job should run.
2022-01-05 14:38:38 -05:00
func ( r * CredentialRevocationJob ) NextRunIn ( _ context . Context ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
return defaultNextRunIn , nil
}
// Name is the unique name of the job.
func ( r * CredentialRevocationJob ) Name () string {
return credentialRevocationJobName
}
// Description is the human readable description of the job.
func ( r * CredentialRevocationJob ) Description () string {
return "Periodically revokes dynamic credentials that are no longer in use and have been set for revocation (in the revoke state)."
}
// CredentialStoreCleanupJob is the recurring job that deletes Vault credential stores that
// have been soft deleted and tokens have been revoked or expired.
// The CredentialStoreCleanupJob is not thread safe, an attempt to Run the job concurrently
// will result in an JobAlreadyRunning error.
type CredentialStoreCleanupJob struct {
reader db . Reader
writer db . Writer
kms * kms . Kms
limit int
running ua . Bool
numProcessed int
numStores int
}
// newCredentialStoreCleanupJob creates a new in-memory CredentialStoreCleanupJob.
//
// No options are supported.
2023-06-06 17:09:08 -04:00
func newCredentialStoreCleanupJob ( ctx context . Context , r db . Reader , w db . Writer , kms * kms . Kms , opt ... Option ) ( * CredentialStoreCleanupJob , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.newCredentialStoreCleanupJob"
switch {
case r == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Reader" )
2021-06-22 22:26:26 -04:00
case w == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Writer" )
2021-06-22 22:26:26 -04:00
case kms == nil :
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing kms" )
2021-06-22 22:26:26 -04:00
}
opts := getOpts ( opt ... )
if opts . withLimit == 0 {
// zero signals the boundary defaults should be used.
opts . withLimit = db . DefaultLimit
}
return & CredentialStoreCleanupJob {
reader : r ,
writer : w ,
kms : kms ,
limit : opts . withLimit ,
}, nil
}
// Status returns the current status of the credential store cleanup job.
func ( r * CredentialStoreCleanupJob ) Status () scheduler . JobStatus {
return scheduler . JobStatus {
Completed : r . numProcessed ,
Total : r . numStores ,
}
}
// Run deletes all vault credential stores in the repo that have been soft deleted.
// Can not be run in parallel, if Run is invoked while already running an error with code
// JobAlreadyRunning will be returned.
2024-09-20 12:14:29 -04:00
func ( r * CredentialStoreCleanupJob ) Run ( ctx context . Context , _ time . Duration ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(CredentialStoreCleanupJob).Run"
2024-02-13 10:54:48 -05:00
if ! r . running . CompareAndSwap ( r . running . Load (), true ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . JobAlreadyRunning , op , "job already running" )
2021-06-22 22:26:26 -04:00
}
defer r . running . Store ( false )
// Verify context is not done before running
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// TODO (lcr 06/2021): Oplog does not currently support bulk
// operations. Push cleanup to the database once bulk
// operations are added.
var stores [] * CredentialStore
2022-10-25 18:29:38 -05:00
err := r . reader . SearchWhere ( ctx , & stores , credStoreCleanupWhereClause , [] any { RevokeToken }, db . WithLimit ( r . limit ))
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
// Set numProcessed and numStores for status report
r . numProcessed , r . numStores = 0 , len ( stores )
for _ , store := range stores {
// Verify context is not done before renewing next token
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2022-07-20 16:24:46 -07:00
oplogWrapper , err := r . kms . GetWrapper ( ctx , store . ProjectId , kms . KeyPurposeOplog )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-28 06:31:16 -04:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "unable to get oplog wrapper for credential store cleanup job" , "credential store id" , store . PublicId ))
2021-06-22 22:26:26 -04:00
r . numProcessed ++
continue
}
_ , err = r . writer . Delete ( ctx , store , db . WithOplog ( oplogWrapper , store . oplog ( oplog . OpType_OP_TYPE_DELETE )))
if err != nil {
2021-07-28 06:31:16 -04:00
event . WriteError ( ctx , op , err , event . WithInfoMsg ( "error deleting credential store" , "credential store id" , store . PublicId ))
2021-06-22 22:26:26 -04:00
}
r . numProcessed ++
}
return nil
}
// NextRunIn determine when the next credential store cleanup job should run.
2022-01-05 14:38:38 -05:00
func ( r * CredentialStoreCleanupJob ) NextRunIn ( _ context . Context ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
return defaultNextRunIn , nil
}
// Name is the unique name of the job.
func ( r * CredentialStoreCleanupJob ) Name () string {
return credentialStoreCleanupJobName
}
// Description is the human readable description of the job.
func ( r * CredentialStoreCleanupJob ) Description () string {
return "Periodically deletes Vault credential stores that have been soft deleted and tokens have been revoked or expired."
}
// CredentialCleanupJob is the recurring job that deletes Vault credentials that are no longer
// attached to a session (have a null session_id) and are not active.
// The CredentialCleanupJob is not thread safe, an attempt to Run the job concurrently
// will result in an JobAlreadyRunning error.
type CredentialCleanupJob struct {
writer db . Writer
running ua . Bool
numCreds int
}
// newCredentialCleanupJob creates a new in-memory CredentialCleanupJob.
//
// No options are supported.
2023-06-06 17:09:08 -04:00
func newCredentialCleanupJob ( ctx context . Context , w db . Writer ) ( * CredentialCleanupJob , error ) {
2021-06-22 22:26:26 -04:00
const op = "vault.newCredentialCleanupJob"
if w == nil {
2023-06-06 17:09:08 -04:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing db.Writer" )
2021-06-22 22:26:26 -04:00
}
return & CredentialCleanupJob {
writer : w ,
}, nil
}
// Status returns the current status of the credential cleanup job.
func ( r * CredentialCleanupJob ) Status () scheduler . JobStatus {
// Cleanup runs a single exec command to the database, therefore completed and total
// are both set to numCreds.
return scheduler . JobStatus {
Completed : r . numCreds ,
Total : r . numCreds ,
}
}
// Run deletes all Vault credential in the repo that have a null session_id and are not active.
// Can not be run in parallel, if Run is invoked while already running an error with code
// JobAlreadyRunning will be returned.
2024-09-20 12:14:29 -04:00
func ( r * CredentialCleanupJob ) Run ( ctx context . Context , _ time . Duration ) error {
2021-06-22 22:26:26 -04:00
const op = "vault.(CredentialCleanupJob).Run"
2024-02-13 10:54:48 -05:00
if ! r . running . CompareAndSwap ( r . running . Load (), true ) {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . JobAlreadyRunning , op , "job already running" )
2021-06-22 22:26:26 -04:00
}
defer r . running . Store ( false )
// Verify context is not done before running
if err := ctx . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
numRows , err := r . writer . Exec ( ctx , credCleanupQuery , nil )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
r . numCreds = numRows
return nil
}
// NextRunIn determine when the next credential cleanup job should run.
2022-01-05 14:38:38 -05:00
func ( r * CredentialCleanupJob ) NextRunIn ( _ context . Context ) ( time . Duration , error ) {
2021-06-22 22:26:26 -04:00
return defaultNextRunIn , nil
}
// Name is the unique name of the job.
func ( r * CredentialCleanupJob ) Name () string {
return credentialCleanupJobName
}
// Description is the human readable description of the job.
func ( r * CredentialCleanupJob ) Description () string {
return "Periodically deletes Vault credentials that are no longer attached to a session (have a null session_id) and are not active in Vault."
}