From 1a89c04d580673b1aa109ee03b5159df985e0a1d Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 10 Apr 2024 15:19:26 -0700 Subject: [PATCH 1/2] remove rotation manager from CE --- builtin/logical/aws/backend.go | 2 +- sdk/framework/backend.go | 9 +- .../{root_credential.go => rotation_job.go} | 0 sdk/logical/system_view.go | 6 +- sdk/plugin/pb/backend.proto | 7 - vault/dynamic_system_view.go | 2 +- vault/rotation.go | 380 ------------------ vault/rotation_stubs_oss.go | 27 ++ 8 files changed, 37 insertions(+), 396 deletions(-) rename sdk/logical/{root_credential.go => rotation_job.go} (100%) delete mode 100644 vault/rotation.go create mode 100644 vault/rotation_stubs_oss.go diff --git a/builtin/logical/aws/backend.go b/builtin/logical/aws/backend.go index 1a46bc91ca8e..095c4a838b33 100644 --- a/builtin/logical/aws/backend.go +++ b/builtin/logical/aws/backend.go @@ -65,7 +65,7 @@ func Backend(_ *logical.BackendConfig) *backend { }, // placeholder - RotatePassword: func(ctx context.Context, request *logical.Request) error { + RotateCredential: func(ctx context.Context, request *logical.Request) error { fmt.Print("aws.RotatePassword called\n") return nil }, diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index 956696da7e12..4cf89394db53 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -109,8 +109,9 @@ type Backend struct { // RunningVersion is the optional version that will be self-reported RunningVersion string - // Functions for rotating the root password of a backend if it exists - RotatePassword func(context.Context, *logical.Request) error // specific backend developer responsible for handling basically everything + // RotateCredential is the callback function used by the RotationManager + // to communicate with a plugin on when to rotate a credential + RotateCredential func(context.Context, *logical.Request) error logger log.Logger system logical.SystemView @@ -670,11 +671,11 @@ func (b *Backend) handleRollback(ctx context.Context, req *logical.Request) (*lo // handleRotation invokes the RotatePassword func set on the backend. func (b *Backend) handleRotation(ctx context.Context, req *logical.Request) (*logical.Response, error) { - if b.RotatePassword == nil { + if b.RotateCredential == nil { return nil, logical.ErrUnsupportedOperation } - err := b.RotatePassword(ctx, req) + err := b.RotateCredential(ctx, req) if err != nil { return nil, err } diff --git a/sdk/logical/root_credential.go b/sdk/logical/rotation_job.go similarity index 100% rename from sdk/logical/root_credential.go rename to sdk/logical/rotation_job.go diff --git a/sdk/logical/system_view.go b/sdk/logical/system_view.go index 44e7e6885e72..08b99561c0da 100644 --- a/sdk/logical/system_view.go +++ b/sdk/logical/system_view.go @@ -101,6 +101,7 @@ type SystemView interface { // GenerateIdentityToken returns an identity token for the requesting plugin. GenerateIdentityToken(ctx context.Context, req *pluginutil.IdentityTokenRequest) (*pluginutil.IdentityTokenResponse, error) + // RegisterRotationJob returns a rotation ID for a requested plugin credential. RegisterRotationJob(ctx context.Context, reqPath string, job *RotationJob) (rotationID string, err error) } @@ -288,7 +289,6 @@ func (d StaticSystemView) APILockShouldBlockRequest() (bool, error) { return d.APILockShouldBlockRequestVal, nil } -func (d StaticSystemView) RegisterRotationJob(ctx context.Context, reqPath string, job *RotationJob) (rotationID string, err error) { - return "", nil - // return "", errors.New("RegisterRotationJob is not implemented in StaticSystemView") +func (d StaticSystemView) RegisterRotationJob(_ context.Context, _ string, _ *RotationJob) (rotationID string, err error) { + return "", errors.New("RegisterRotationJob is not implemented in StaticSystemView") } diff --git a/sdk/plugin/pb/backend.proto b/sdk/plugin/pb/backend.proto index c91db9bbdd8b..5d0c4cf9432e 100644 --- a/sdk/plugin/pb/backend.proto +++ b/sdk/plugin/pb/backend.proto @@ -625,13 +625,6 @@ message RotationJobInput { string name = 4; } -//message RotationScheduleInput { -// google.protobuf.Struct schedule = 1; -// google.protobuf.Duration rotation_window = 2; -// string rotation_schedule = 3; -// google.protobuf.Struct next_vault_rotation = 4; -//} - // SystemView exposes system configuration information in a safe way for plugins // to consume. Plugins should implement the client for this service. service SystemView { diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index 4a00d1aa86c3..5bfa2e2a0db0 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -473,7 +473,7 @@ func (d dynamicSystemView) RegisterRotationJob(ctx context.Context, reqPath stri path = ns.Path + "/" + reqPath } - id, err := d.core.rotationManager.Register(namespace.ContextWithNamespace(ctx, job.Namespace), path, job) + id, err := d.core.RegisterRotationJob(namespace.ContextWithNamespace(ctx, job.Namespace), path, job) if err != nil { return "", fmt.Errorf("error registering rotation job: %s", err) } diff --git a/vault/rotation.go b/vault/rotation.go deleted file mode 100644 index 4f375b22e1ac..000000000000 --- a/vault/rotation.go +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1package vault - -package vault - -import ( - "context" - "errors" - "fmt" - "os" - "path" - "strconv" - "sync" - "time" - - "github.com/hashicorp/vault/helper/fairshare" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/sdk/helper/base62" - "github.com/hashicorp/vault/sdk/logical" - - log "github.com/hashicorp/go-hclog" - - "github.com/hashicorp/vault/sdk/queue" -) - -const ( - fairshareRotationWorkersOverrideVar = "VAULT_CREDENTIAL_ROTATION_WORKERS" -) - -type RotationManager struct { - core *Core - logger log.Logger - mu sync.Mutex - - jobManager *fairshare.JobManager - queue *queue.PriorityQueue - done chan struct{} - quitContext context.Context - - router *Router - backends func() *[]MountEntry // list of logical and auth backends, remember to call RUnlock -} - -// rotationEntry is used to structure the values the expiration -// manager stores. This is used to handle renew and revocation. -type rotationEntry struct { - RotationID string `json:"rotation_id"` - Path string `json:"path"` - Data map[string]interface{} `json:"data"` - RotationJob *logical.RotationJob `json:"static_secret"` - IssueTime time.Time `json:"issue_time"` - ExpireTime time.Time `json:"expire_time"` - - Namespace *namespace.Namespace -} - -func (rm *RotationManager) Start() error { - done := rm.done - go func() { - ticker := time.NewTicker(10 * time.Second) - rm.logger.Info("started ticker") - for { - // rm.mu.Lock() - select { - case <-done: - rm.logger.Debug("done with loop; received from channel") - return - case t := <-ticker.C: - rm.logger.Info("time", "time", t.Format(time.RFC3339)) - err := rm.CheckQueue() - if err != nil { - rm.logger.Error("check queue error", "err", err) - } - } - } - }() - return nil -} - -// Stop is used to prevent further automatic rotations. -func (rm *RotationManager) Stop() error { - // Stop all the pending rotation timers - rm.logger.Debug("stop triggered") - defer rm.logger.Debug("finished stopping") - - rm.jobManager.Stop() - - // close done channel - close(rm.done) - - return nil -} - -func (rm *RotationManager) CheckQueue() error { - // loop runs forever, so break whenever you get to the first credential that doesn't need updating - for { - now := time.Now() - i, err := rm.queue.Pop() - if err != nil { - rm.logger.Info("automated rotation queue empty", "err", err) - return nil - } - - if i.Priority > now.Unix() { - rm.logger.Debug("Item not ready for rotation; adding back to queue") - err := rm.queue.Push(i) - if err != nil { - // this is pretty bad because we have no real way to fix it and save the item, but the Push operation only - // errors on malformed items, which shouldn't be possible here - return err - } - break // this item is not ripe yet, which means all later items are also unripe, so exit the check loop - } - - // var re *rotationEntry - re, ok := i.Value.(*rotationEntry) - if !ok { - return fmt.Errorf("error parsing rotation entry from queue") - } - - // re = entry - - rm.logger.Debug("check", "window", re.RotationJob.Schedule.RotationWindow, "time", re.RotationJob.Schedule.NextVaultRotation) - if !logical.DefaultScheduler.IsInsideRotationWindow(re.RotationJob.Schedule, now) { - rm.logger.Debug("Not inside rotation window, pushing back to queue") - err := rm.queue.Push(i) - if err != nil { - // this is pretty bad because we have no real way to fix it and save the item, but the Push operation only - // errors on malformed items, which shouldn't be possible here - return err - } - // don't break here, since the heap is keyed on priority, which is just timestamp. - // it's possible for a later item to be ready for rotation, so we need to keep going - } - rm.logger.Debug("Item ready for rotation; making rotation request to sdk/backend") - // do rotation - req := &logical.Request{ - Operation: logical.RotationOperation, - Path: re.Path, - } - - rm.jobManager.AddJob(&rotationJob{ - rm: rm, - req: req, - entry: re, - }, "best-queue-ever") - } - - return nil -} - -// Register takes a request and response with an associated StaticSecret. The -// secret gets assigned a RotationID and the management of the rotation is -// assumed by the rotation manager. -func (rm *RotationManager) Register(ctx context.Context, reqPath string, job *logical.RotationJob) (id string, retErr error) { - rm.logger.Debug("Starting registration") - - // Ignore if there is no rotation job - if job == nil { - return "", nil - } - - rotationID := job.RotationID - var re *rotationEntry - - // either create a new rotationEntry or get the existing one - if rotationID != "" { - rm.logger.Debug("rotationID detected, this is an update", "rotationID", rotationID) - entry, err := rm.queue.PopByKey(rotationID) - if err != nil { - rm.logger.Warn("error popping item", "rotation_id", rotationID, "err", err) - return "", err - } - if entry != nil { - // this is an update - re = entry.Value.(*rotationEntry) - } - } else { - // Create a rotation entry. We use TokenLength because that is what is used - // by ExpirationManager - - // TODO: Check if we need to validate the root credential - - rotationRand, err := base62.Random(TokenLength) - if err != nil { - return "", err - } - ns, err := namespace.FromContext(ctx) - rotationID = path.Join(reqPath, rotationRand) - if ns.ID != namespace.RootNamespaceID { - rotationID = fmt.Sprintf("%s.%s", rotationID, ns.ID) - } - re = &rotationEntry{ - RotationID: rotationID, - Path: reqPath, - RotationJob: job, - Namespace: ns, - } - } - - issueTime := time.Now() - expireTime := time.Now() - if job.Schedule.Schedule != nil { - expireTime = logical.DefaultScheduler.NextRotationTimeFromInput(job.Schedule, time.Now()) - job.Schedule.NextVaultRotation = expireTime - } - rm.logger.Debug("SCHEDULE", "VALUE", job.Schedule.RotationSchedule) - rm.logger.Debug("WINDOW", "VALUE", job.Schedule.RotationWindow) - rm.logger.Debug("TTL", "VALUE", job.Schedule.TTL) - re.ExpireTime = expireTime - re.IssueTime = issueTime - - // lock and populate the queue - // @TODO figure out why locking is leading to infinite loop - // r.core.stateLock.Lock() - - rm.logger.Debug("Creating queue item") - rm.logger.Debug("next rotation time", "exp", expireTime.Format(time.RFC3339)) - - // @TODO for different cases, update rotation entry if it is already in queue - // for now, assuming it is a fresh root credential and the schedule is not being updated - item := &queue.Item{ - Key: re.RotationID, - Value: re, - Priority: re.ExpireTime.Unix(), - } - - rm.logger.Debug("Pushing item into credential queue") - - if err := rm.queue.Push(item); err != nil { - // TODO handle error - rm.logger.Debug("Error pushing item into credential queue") - return "", err - } - - // r.core.stateLock.Unlock() - rm.logger.Debug("", "rotationID", re.RotationID) - return re.RotationID, nil -} - -func getNumRotationWorkers(c *Core, l log.Logger) int { - numWorkers := c.numExpirationWorkers - - workerOverride := os.Getenv(fairshareRotationWorkersOverrideVar) - if workerOverride != "" { - i, err := strconv.Atoi(workerOverride) - if err != nil { - l.Warn("vault rotation workers override must be an integer", "value", workerOverride) - } else if i < 1 || i > 10000 { - l.Warn("vault rotation workers override out of range", "value", i) - } else { - numWorkers = i - } - } - - return numWorkers -} - -func (c *Core) startRotation() error { - logger := c.baseLogger.Named("rotation-job-manager") - - jobManager := fairshare.NewJobManager("rotate", getNumRotationWorkers(c, logger), logger, c.metricSink) - jobManager.Start() - - c.AddLogger(logger) - c.rotationManager = &RotationManager{ - core: c, - logger: logger, - // TODO figure out how to populate this if credentials already exist after unseal - queue: queue.New(), - done: make(chan struct{}), - jobManager: jobManager, - quitContext: c.activeContext, - router: c.router, - } - err := c.rotationManager.Start() - if err != nil { - return err - } - return nil -} - -// stopRotation is used to stop the rotation manager before -// sealing Vault. -func (c *Core) stopRotation() error { - if c.rotationManager != nil { - if err := c.rotationManager.Stop(); err != nil { - return err - } - c.metricsMutex.Lock() - defer c.metricsMutex.Unlock() - c.rotationManager = nil - } - return nil -} - -// rotationJob implements fairshare.Job -// -// if you do queue management here you _must_ lock -type rotationJob struct { - rm *RotationManager - req *logical.Request - entry *rotationEntry -} - -// Execute is an implementation of fairshare.Job.Execute and in this case handles requesting rotation from -// the backend. It will return an error both in the case of a direct error, and in the case of certain kinds -// of error-shaped logical.Response returns. -func (j *rotationJob) Execute() error { - j.rm.logger.Debug("path", "path", j.entry.Path) - j.rm.logger.Debug("rpath", "rpath", j.req.Path) - ctx := namespace.ContextWithNamespace(j.rm.quitContext, j.entry.Namespace) - _, err := j.rm.router.Route(ctx, j.req) - - // TODO: clean up this branch - if errors.Is(err, logical.ErrUnsupportedOperation) { - j.rm.logger.Info("unsupported") - return err - } else if err != nil { - // requeue with backoff - j.rm.logger.Info("other rotate error", "err", err) - return err - } - - // TODO: inspect logical.Response for other error-y things (there may not be any) - - // success - j.rm.logger.Debug("Successfully called rotate root code for backend") - issueTime := time.Now() - j.entry.RotationJob.Schedule.LastVaultRotation = issueTime - expireTime := logical.DefaultScheduler.NextRotationTime(j.entry.RotationJob.Schedule) - newEntry := &rotationEntry{ - RotationID: j.entry.RotationID, - Path: j.entry.Path, - Data: j.entry.Data, - RotationJob: j.entry.RotationJob, - IssueTime: issueTime, - // expires the next time the schedule is activated from the issue time - ExpireTime: expireTime, - Namespace: j.entry.Namespace, - } - j.entry.RotationJob.Schedule.NextVaultRotation = newEntry.ExpireTime - - // lock and populate the queue - j.rm.mu.Lock() - - item := &queue.Item{ - // will preserve same rotation ID, only updating Value, Priority with new rotation time - Key: newEntry.RotationID, - Value: newEntry, - Priority: newEntry.ExpireTime.Unix(), - } - - j.rm.logger.Debug("Pushing item into credential queue") - j.rm.logger.Debug("will rotate at", "time", newEntry.ExpireTime.Format(time.RFC3339)) - - if err := j.rm.queue.Push(item); err != nil { - // TODO handle error - j.rm.logger.Debug("Error pushing item into credential queue") - return err - } - j.rm.mu.Unlock() - - return nil -} - -// OnFailure implements the OnFailure interface method and requeues with a backoff when it happens -func (j *rotationJob) OnFailure(err error) { - j.rm.logger.Info("rotation failed, requeuing", "error", err) - - err = j.rm.queue.Push(&queue.Item{ - Key: j.entry.RotationID, - Value: j.entry, - Priority: time.Now().Add(10 * time.Second).Unix(), // TODO: Configure this - }) - // an error here is really bad because we can't really fix it and will lose the rotation entry as a result. - if err != nil { - j.rm.logger.Error("can't requeue an item", "id", j.entry.RotationID) - } -} diff --git a/vault/rotation_stubs_oss.go b/vault/rotation_stubs_oss.go new file mode 100644 index 000000000000..1a13d6534b2a --- /dev/null +++ b/vault/rotation_stubs_oss.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1package vault + +package vault + +import ( + "context" + "errors" + + "github.com/hashicorp/vault/sdk/logical" +) + +var ErrRotationManagerUnsupported = errors.New("rotation manager capabilities not supported in Vault community edition") + +type RotationManager struct{} + +func (c *Core) startRotation() error { + return nil +} + +func (c *Core) stopRotation() error { + return nil +} + +func (c *Core) RegisterRotationJob(_ context.Context, _ string, _ *logical.RotationJob) (string, error) { + return "", ErrRotationManagerUnsupported +} From 2d72a26ddeda64dcbbd9c01eb28c8908d9cdad9f Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 10 Apr 2024 15:26:57 -0700 Subject: [PATCH 2/2] fix typos --- builtin/credential/ldap/backend.go | 2 +- builtin/credential/ldap/path_config_rotate_root.go | 2 +- builtin/logical/aws/backend.go | 2 +- sdk/framework/backend.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin/credential/ldap/backend.go b/builtin/credential/ldap/backend.go index 506b2c4963ad..6b37319458e0 100644 --- a/builtin/credential/ldap/backend.go +++ b/builtin/credential/ldap/backend.go @@ -62,7 +62,7 @@ func Backend() *backend { AuthRenew: b.pathLoginRenew, BackendType: logical.TypeCredential, - RotatePassword: func(ctx context.Context, req *logical.Request) error { + RotateCredential: func(ctx context.Context, req *logical.Request) error { // lock the backend's state - really just the config state - for mutating b.mu.Lock() defer b.mu.Unlock() diff --git a/builtin/credential/ldap/path_config_rotate_root.go b/builtin/credential/ldap/path_config_rotate_root.go index a0f3ad38e616..9a59bbbe23f5 100644 --- a/builtin/credential/ldap/path_config_rotate_root.go +++ b/builtin/credential/ldap/path_config_rotate_root.go @@ -52,7 +52,7 @@ func (b *backend) pathConfigRotateRootUpdate(ctx context.Context, req *logical.R b.mu.RUnlock() - err = b.RotatePassword(ctx, req) + err = b.RotateCredential(ctx, req) if err != nil { return nil, err } diff --git a/builtin/logical/aws/backend.go b/builtin/logical/aws/backend.go index 095c4a838b33..2cef5384ac83 100644 --- a/builtin/logical/aws/backend.go +++ b/builtin/logical/aws/backend.go @@ -66,7 +66,7 @@ func Backend(_ *logical.BackendConfig) *backend { // placeholder RotateCredential: func(ctx context.Context, request *logical.Request) error { - fmt.Print("aws.RotatePassword called\n") + fmt.Print("aws.RotateCredential called\n") return nil }, diff --git a/sdk/framework/backend.go b/sdk/framework/backend.go index 4cf89394db53..5c7fa3155d10 100644 --- a/sdk/framework/backend.go +++ b/sdk/framework/backend.go @@ -669,7 +669,7 @@ func (b *Backend) handleRollback(ctx context.Context, req *logical.Request) (*lo return resp, merr.ErrorOrNil() } -// handleRotation invokes the RotatePassword func set on the backend. +// handleRotation invokes the RotateCredential func set on the backend. func (b *Backend) handleRotation(ctx context.Context, req *logical.Request) (*logical.Response, error) { if b.RotateCredential == nil { return nil, logical.ErrUnsupportedOperation