mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
956 lines
31 KiB
956 lines
31 KiB
package vault
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/observability/event"
|
|
"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
|
|
)
|
|
|
|
func RegisterJobs(ctx context.Context, scheduler *scheduler.Scheduler, r db.Reader, w db.Writer, kms *kms.Kms) error {
|
|
const op = "vault.RegisterJobs"
|
|
tokenRenewal, err := newTokenRenewalJob(r, w, kms)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err = scheduler.RegisterJob(ctx, tokenRenewal); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("token renewal job"))
|
|
}
|
|
tokenRevoke, err := newTokenRevocationJob(r, w, kms)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err = scheduler.RegisterJob(ctx, tokenRevoke); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("token revocation job"))
|
|
}
|
|
credRenewal, err := newCredentialRenewalJob(r, w, kms)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err = scheduler.RegisterJob(ctx, credRenewal); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("credential renewal job"))
|
|
}
|
|
credRevoke, err := newCredentialRevocationJob(r, w, kms)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err = scheduler.RegisterJob(ctx, credRevoke); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("credential revocation job"))
|
|
}
|
|
credStoreCleanup, err := newCredentialStoreCleanupJob(r, w, kms)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err = scheduler.RegisterJob(ctx, credStoreCleanup); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("credential store cleanup job"))
|
|
}
|
|
credCleanup, err := newCredentialCleanupJob(w)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err = scheduler.RegisterJob(ctx, credCleanup); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("credential cleanup job"))
|
|
}
|
|
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.
|
|
func newTokenRenewalJob(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*TokenRenewalJob, error) {
|
|
const op = "vault.newTokenRenewalJob"
|
|
switch {
|
|
case r == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Reader")
|
|
case w == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Writer")
|
|
case kms == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms")
|
|
}
|
|
|
|
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.
|
|
func (r *TokenRenewalJob) Run(ctx context.Context) error {
|
|
const op = "vault.(TokenRenewalJob).Run"
|
|
if !r.running.CAS(r.running.Load(), true) {
|
|
return errors.New(ctx, errors.JobAlreadyRunning, op, "job already running")
|
|
}
|
|
defer r.running.Store(false)
|
|
|
|
// Verify context is not done before running
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
var ps []*privateStore
|
|
// 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.
|
|
err := r.reader.SearchWhere(ctx, &ps, `token_renewal_time < wt_add_seconds_to_now(?)`, []interface{}{renewalWindow.Seconds()}, db.WithLimit(r.limit))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Set numProcessed and numTokens for status report
|
|
r.numProcessed, r.numTokens = 0, len(ps)
|
|
|
|
for _, s := range ps {
|
|
// Verify context is not done before renewing next token
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err := r.renewToken(ctx, s); err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error renewing token", "credential store id", s.StoreId, "token status", s.TokenStatus))
|
|
}
|
|
r.numProcessed++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *TokenRenewalJob) renewToken(ctx context.Context, s *privateStore) error {
|
|
const op = "vault.(TokenRenewalJob).renewToken"
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, s.ScopeId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
if err = s.decrypt(ctx, databaseWrapper); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
token := s.token()
|
|
if token == nil {
|
|
// Store has no token to renew
|
|
return nil
|
|
}
|
|
|
|
vc, err := s.client()
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
var respErr *vault.ResponseError
|
|
renewedToken, err := vc.renewToken()
|
|
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 {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if numRows != 1 {
|
|
return errors.New(ctx, errors.Unknown, op, "token expired but failed to update repo")
|
|
}
|
|
if s.TokenStatus == string(CurrentToken) {
|
|
event.WriteSysEvent(ctx, op, "Vault credential store current token has expired", "credential store id", s.StoreId)
|
|
}
|
|
|
|
// Set credentials associated with this token to expired as Vault will already cascade delete them
|
|
_, err = r.writer.Exec(ctx, updateCredentialStatusByTokenQuery, []interface{}{ExpiredCredential, token.TokenHmac})
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("error updating credentials to revoked after revoking token"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to renew vault token"))
|
|
}
|
|
|
|
tokenExpires, err := renewedToken.TokenTTL()
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get vault token expiration"))
|
|
}
|
|
|
|
token.expiration = tokenExpires
|
|
query, values := token.updateExpirationQuery()
|
|
numRows, err := r.writer.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if numRows != 1 {
|
|
return errors.New(ctx, errors.Unknown, op, "token renewed but failed to update repo")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextRunIn queries the vault credential repo to determine when the next token renewal job should run.
|
|
func (r *TokenRenewalJob) NextRunIn(ctx context.Context) (time.Duration, error) {
|
|
const op = "vault.(TokenRenewalJob).NextRunIn"
|
|
next, err := nextRenewal(ctx, r)
|
|
if err != nil {
|
|
return defaultNextRunIn, errors.WrapDeprecated(err, op)
|
|
}
|
|
|
|
return next, nil
|
|
}
|
|
|
|
func nextRenewal(ctx context.Context, j scheduler.Job) (time.Duration, error) {
|
|
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:
|
|
return 0, errors.NewDeprecated(errors.Unknown, op, "unknown job")
|
|
}
|
|
|
|
rows, err := r.Query(context.Background(), query, nil)
|
|
if err != nil {
|
|
return 0, errors.WrapDeprecated(err, op)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
type NextRenewal struct {
|
|
RenewalIn time.Duration
|
|
}
|
|
var n NextRenewal
|
|
err = r.ScanRows(ctx, rows, &n)
|
|
if err != nil {
|
|
return 0, errors.WrapDeprecated(err, op)
|
|
}
|
|
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
|
|
}
|
|
|
|
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.
|
|
func newTokenRevocationJob(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*TokenRevocationJob, error) {
|
|
const op = "vault.newTokenRevocationJob"
|
|
switch {
|
|
case r == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Reader")
|
|
case w == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Writer")
|
|
case kms == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms")
|
|
}
|
|
|
|
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.
|
|
func (r *TokenRevocationJob) Run(ctx context.Context) error {
|
|
const op = "vault.(TokenRevocationJob).Run"
|
|
if !r.running.CAS(r.running.Load(), true) {
|
|
return errors.New(ctx, errors.JobAlreadyRunning, op, "job already running")
|
|
}
|
|
defer r.running.Store(false)
|
|
|
|
// Verify context is not done before running
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// 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'
|
|
))
|
|
`
|
|
|
|
var ps []*privateStore
|
|
err := r.reader.SearchWhere(ctx, &ps, where, nil, db.WithLimit(r.limit))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Set numProcessed and numTokens for s report
|
|
r.numProcessed, r.numTokens = 0, len(ps)
|
|
for _, s := range ps {
|
|
// Verify context is not done before renewing next token
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err := r.revokeToken(ctx, s); err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error revoking token", "credential store id", s.StoreId))
|
|
}
|
|
r.numProcessed++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *TokenRevocationJob) revokeToken(ctx context.Context, s *privateStore) error {
|
|
const op = "vault.(TokenRevocationJob).revokeToken"
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, s.ScopeId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
if err = s.decrypt(ctx, databaseWrapper); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
token := s.token()
|
|
if token == nil {
|
|
// Store has no token to revoke
|
|
return nil
|
|
}
|
|
|
|
vc, err := s.client()
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
var respErr *vault.ResponseError
|
|
err = vc.revokeToken()
|
|
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 {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to revoke vault token"))
|
|
}
|
|
|
|
query, values := token.updateStatusQuery(RevokedToken)
|
|
numRows, err := r.writer.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if numRows != 1 {
|
|
return errors.New(ctx, errors.Unknown, op, "token revoked but failed to update repo")
|
|
}
|
|
|
|
// Set credentials associated with this token to revoked as Vault will already cascade revoke them
|
|
_, err = r.writer.Exec(ctx, updateCredentialStatusByTokenQuery, []interface{}{RevokedCredential, token.TokenHmac})
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("error updating credentials to revoked after revoking token"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextRunIn determines when the next token revocation job should run.
|
|
func (r *TokenRevocationJob) NextRunIn(_ context.Context) (time.Duration, error) {
|
|
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.
|
|
func newCredentialRenewalJob(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*CredentialRenewalJob, error) {
|
|
const op = "vault.newCredentialRenewalJob"
|
|
switch {
|
|
case r == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Reader")
|
|
case w == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Writer")
|
|
case kms == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms")
|
|
}
|
|
|
|
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.
|
|
func (r *CredentialRenewalJob) Run(ctx context.Context) error {
|
|
const op = "vault.(CredentialRenewalJob).Run"
|
|
if !r.running.CAS(r.running.Load(), true) {
|
|
return errors.New(ctx, errors.JobAlreadyRunning, op, "job already running")
|
|
}
|
|
defer r.running.Store(false)
|
|
|
|
// Verify context is not done before running
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
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.
|
|
err := r.reader.SearchWhere(ctx, &creds, `renewal_time < wt_add_seconds_to_now(?) and status = ?`, []interface{}{renewalWindow.Seconds(), ActiveCredential}, db.WithLimit(r.limit))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// 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 {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
if err := r.renewCred(ctx, c); err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error renewing credential", "credential id", c.PublicId))
|
|
}
|
|
|
|
r.numProcessed++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *CredentialRenewalJob) renewCred(ctx context.Context, c *privateCredential) error {
|
|
const op = "vault.(CredentialRenewalJob).renewCred"
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
if err = c.decrypt(ctx, databaseWrapper); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
vc, err := c.client()
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
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())
|
|
renewedCred, err := vc.renewLease(c.ExternalId, leaseDuration)
|
|
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 {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if numRows != 1 {
|
|
return errors.New(ctx, errors.Unknown, op, "credential expired but failed to update repo")
|
|
}
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to renew credential"))
|
|
}
|
|
|
|
cred.expiration = time.Duration(renewedCred.LeaseDuration) * time.Second
|
|
query, values := cred.updateExpirationQuery()
|
|
numRows, err := r.writer.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if numRows != 1 {
|
|
return errors.New(ctx, errors.Unknown, op, "credential renewed but failed to update repo")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextRunIn queries the vault credential repo to determine when the next credential renewal job should run.
|
|
func (r *CredentialRenewalJob) NextRunIn(ctx context.Context) (time.Duration, error) {
|
|
const op = "vault.(CredentialRenewalJob).NextRunIn"
|
|
next, err := nextRenewal(ctx, r)
|
|
if err != nil {
|
|
return defaultNextRunIn, errors.WrapDeprecated(err, op)
|
|
}
|
|
|
|
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.
|
|
func newCredentialRevocationJob(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*CredentialRevocationJob, error) {
|
|
const op = "vault.newCredentialRevocationJob"
|
|
switch {
|
|
case r == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Reader")
|
|
case w == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Writer")
|
|
case kms == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms")
|
|
}
|
|
|
|
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.
|
|
func (r *CredentialRevocationJob) Run(ctx context.Context) error {
|
|
const op = "vault.(CredentialRevocationJob).Run"
|
|
if !r.running.CAS(r.running.Load(), true) {
|
|
return errors.New(ctx, errors.JobAlreadyRunning, op, "job already running")
|
|
}
|
|
defer r.running.Store(false)
|
|
|
|
// Verify context is not done before running
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
var creds []*privateCredential
|
|
err := r.reader.SearchWhere(ctx, &creds, "status = ?", []interface{}{RevokeCredential}, db.WithLimit(r.limit))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// 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 {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if err := r.revokeCred(ctx, c); err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error revoking credential", "credential id", c.PublicId))
|
|
}
|
|
r.numProcessed++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *CredentialRevocationJob) revokeCred(ctx context.Context, c *privateCredential) error {
|
|
const op = "vault.(CredentialRenewalJob).revokeCred"
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
if err = c.decrypt(ctx, databaseWrapper); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
vc, err := c.client()
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
cred := c.toCredential()
|
|
var respErr *vault.ResponseError
|
|
err = vc.revokeLease(c.ExternalId)
|
|
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 {
|
|
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to revoke credential"))
|
|
}
|
|
|
|
query, values := cred.updateStatusQuery(RevokedCredential)
|
|
numRows, err := r.writer.Exec(ctx, query, values)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
if numRows != 1 {
|
|
return errors.New(ctx, errors.Unknown, op, "credential revoked but failed to update repo")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextRunIn determine when the next credential revocation job should run.
|
|
func (r *CredentialRevocationJob) NextRunIn(_ context.Context) (time.Duration, error) {
|
|
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.
|
|
func newCredentialStoreCleanupJob(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*CredentialStoreCleanupJob, error) {
|
|
const op = "vault.newCredentialStoreCleanupJob"
|
|
switch {
|
|
case r == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Reader")
|
|
case w == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Writer")
|
|
case kms == nil:
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms")
|
|
}
|
|
|
|
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.
|
|
func (r *CredentialStoreCleanupJob) Run(ctx context.Context) error {
|
|
const op = "vault.(CredentialStoreCleanupJob).Run"
|
|
if !r.running.CAS(r.running.Load(), true) {
|
|
return errors.New(ctx, errors.JobAlreadyRunning, op, "job already running")
|
|
}
|
|
defer r.running.Store(false)
|
|
|
|
// Verify context is not done before running
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// TODO (lcr 06/2021): Oplog does not currently support bulk
|
|
// operations. Push cleanup to the database once bulk
|
|
// operations are added.
|
|
var stores []*CredentialStore
|
|
err := r.reader.SearchWhere(ctx, &stores, credStoreCleanupWhereClause, []interface{}{RevokeToken}, db.WithLimit(r.limit))
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// 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 {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, store.ScopeId, kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to get oplog wrapper for credential store cleanup job", "credential store id", store.PublicId))
|
|
r.numProcessed++
|
|
continue
|
|
}
|
|
|
|
_, err = r.writer.Delete(ctx, store, db.WithOplog(oplogWrapper, store.oplog(oplog.OpType_OP_TYPE_DELETE)))
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error deleting credential store", "credential store id", store.PublicId))
|
|
}
|
|
|
|
r.numProcessed++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextRunIn determine when the next credential store cleanup job should run.
|
|
func (r *CredentialStoreCleanupJob) NextRunIn(_ context.Context) (time.Duration, error) {
|
|
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.
|
|
func newCredentialCleanupJob(w db.Writer) (*CredentialCleanupJob, error) {
|
|
const op = "vault.newCredentialCleanupJob"
|
|
if w == nil {
|
|
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing db.Writer")
|
|
}
|
|
|
|
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.
|
|
func (r *CredentialCleanupJob) Run(ctx context.Context) error {
|
|
const op = "vault.(CredentialCleanupJob).Run"
|
|
if !r.running.CAS(r.running.Load(), true) {
|
|
return errors.New(ctx, errors.JobAlreadyRunning, op, "job already running")
|
|
}
|
|
defer r.running.Store(false)
|
|
|
|
// Verify context is not done before running
|
|
if err := ctx.Err(); err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
numRows, err := r.writer.Exec(ctx, credCleanupQuery, nil)
|
|
if err != nil {
|
|
return errors.Wrap(ctx, err, op)
|
|
}
|
|
r.numCreds = numRows
|
|
|
|
return nil
|
|
}
|
|
|
|
// NextRunIn determine when the next credential cleanup job should run.
|
|
func (r *CredentialCleanupJob) NextRunIn(_ context.Context) (time.Duration, error) {
|
|
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."
|
|
}
|