Merge pull request #2130 from hashicorp/irindos-rotate-roots
feature(WorkerAuth): Create RotateRoots domain and job
This commit is contained in:
6 files changed
+472
-10
No files matched your search
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/scheduler"
|
||||
"github.com/hashicorp/boundary/internal/scheduler/job"
|
||||
"github.com/hashicorp/boundary/internal/servers"
|
||||
serversjob "github.com/hashicorp/boundary/internal/servers/job"
|
||||
"github.com/hashicorp/boundary/internal/servers/store"
|
||||
"github.com/hashicorp/boundary/internal/session"
|
||||
"github.com/hashicorp/boundary/internal/target"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-secure-stdlib/mlock"
|
||||
"github.com/hashicorp/go-secure-stdlib/pluginutil/v2"
|
||||
"github.com/hashicorp/nodeenrollment/rotation"
|
||||
"github.com/hashicorp/nodeenrollment/types"
|
||||
"github.com/mr-tron/base58"
|
||||
ua "go.uber.org/atomic"
|
||||
@@ -317,15 +317,6 @@ func (c *Controller) Start() error {
|
||||
|
||||
c.baseContext, c.baseCancel = context.WithCancel(context.Background())
|
||||
|
||||
workerAuthStorage, err := c.WorkerAuthRepoStorageFn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching worker auth storage: %w", err)
|
||||
}
|
||||
_, err = rotation.RotateRootCertificates(c.baseContext, workerAuthStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.registerJobs(); err != nil {
|
||||
return fmt.Errorf("error registering jobs: %w", err)
|
||||
}
|
||||
@@ -377,6 +368,9 @@ func (c *Controller) registerJobs() error {
|
||||
if err := session.RegisterJobs(c.baseContext, c.scheduler, rw, c.conf.StatusGracePeriodDuration); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := serversjob.RegisterJobs(c.baseContext, c.scheduler, rw, rw, c.kms); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/scheduler"
|
||||
)
|
||||
|
||||
// RegisterJobs registers the rotate roots job with the provided scheduler.
|
||||
func RegisterJobs(ctx context.Context, scheduler *scheduler.Scheduler, r db.Reader, w db.Writer, kms *kms.Kms) error {
|
||||
const op = "servers.(Jobs).RegisterJobs"
|
||||
|
||||
if isNil(scheduler) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing scheduler")
|
||||
}
|
||||
if isNil(r) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing reader")
|
||||
}
|
||||
if isNil(w) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing writer")
|
||||
}
|
||||
if kms == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing kms")
|
||||
}
|
||||
|
||||
rotateRootsJob, err := newRotateRootsJob(ctx, r, w, kms)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err = scheduler.RegisterJob(ctx, rotateRootsJob); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isNil(i interface{}) bool {
|
||||
if i == nil {
|
||||
return true
|
||||
}
|
||||
switch reflect.TypeOf(i).Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
|
||||
return reflect.ValueOf(i).IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/scheduler"
|
||||
"github.com/hashicorp/boundary/internal/servers"
|
||||
)
|
||||
|
||||
const rotateFrequency = time.Hour
|
||||
|
||||
// rotateRootsJob defines a periodic job that initiates root certificate rotation
|
||||
// It runs every hour; the root rotation function in the library is designed to not
|
||||
// do anything if it's not time to rotate (roots are within their valid ranges)
|
||||
type rotateRootsJob struct {
|
||||
workerAuthRepo *servers.WorkerAuthRepositoryStorage
|
||||
|
||||
totalRotates int
|
||||
}
|
||||
|
||||
// newRotateRootsJob instantiates the rotate roots job.
|
||||
func newRotateRootsJob(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms) (*rotateRootsJob, error) {
|
||||
const op = "servers.newRotateRootsJob"
|
||||
switch {
|
||||
case isNil(r):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing reader")
|
||||
case isNil(w):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing writer")
|
||||
case kms == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
|
||||
}
|
||||
|
||||
workerAuthRepo, err := servers.NewRepositoryStorage(ctx, r, w, kms)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return &rotateRootsJob{
|
||||
workerAuthRepo: workerAuthRepo,
|
||||
totalRotates: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Name returns a short, unique name for the job.
|
||||
func (r *rotateRootsJob) Name() string { return "rotate_roots" }
|
||||
|
||||
// Description returns the description for the job.
|
||||
func (r *rotateRootsJob) Description() string {
|
||||
return "Rotate root certificates"
|
||||
}
|
||||
|
||||
// NextRunIn returns the next run time after a job is completed.
|
||||
// This is represented by RotateFrequency
|
||||
func (r *rotateRootsJob) NextRunIn(_ context.Context) (time.Duration, error) {
|
||||
return rotateFrequency, nil
|
||||
}
|
||||
|
||||
// Status returns the status of the running job.
|
||||
func (r *rotateRootsJob) Status() scheduler.JobStatus {
|
||||
return scheduler.JobStatus{
|
||||
Completed: r.totalRotates,
|
||||
Total: r.totalRotates,
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the job by calling the rotateRoots domain function
|
||||
func (r *rotateRootsJob) Run(ctx context.Context) error {
|
||||
const op = "servers.(rotateRootsJob).Run"
|
||||
|
||||
err := servers.RotateRoots(ctx, r.workerAuthRepo)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
r.totalRotates += 1
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/scheduler"
|
||||
"github.com/hashicorp/boundary/internal/servers"
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
"github.com/hashicorp/nodeenrollment/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRotateRootsJob(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
ctx := context.Background()
|
||||
wrapper := db.TestWrapper(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
|
||||
require.NoError(err)
|
||||
|
||||
workerAuthRepo, err := servers.NewRepositoryStorage(ctx, rw, rw, kmsCache)
|
||||
require.NoError(err)
|
||||
|
||||
type args struct {
|
||||
w db.Writer
|
||||
r db.Reader
|
||||
kms *kms.Kms
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
options []servers.Option
|
||||
wantLimit int
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
}{
|
||||
{
|
||||
name: "nil writer",
|
||||
args: args{
|
||||
r: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil reader",
|
||||
args: args{
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil kms",
|
||||
args: args{
|
||||
w: rw,
|
||||
r: rw,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
w: rw,
|
||||
r: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantLimit: db.DefaultLimit,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := newRotateRootsJob(ctx, tt.args.r, tt.args.w, tt.args.kms)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "Unexpected error %s", err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
require.Equal(0, got.totalRotates)
|
||||
rootIds, err := workerAuthRepo.List(ctx, (*types.RootCertificate)(nil))
|
||||
require.NoError(err)
|
||||
assert.Len(rootIds, 0)
|
||||
assert.Equal("rotate_roots", got.Name())
|
||||
assert.Equal("Rotate root certificates", got.Description())
|
||||
nextRun, err := got.NextRunIn(ctx)
|
||||
require.NoError(err)
|
||||
assert.Equal(time.Hour, nextRun)
|
||||
|
||||
// Run job and ensure rotation was performed
|
||||
err = got.Run(ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(1, got.totalRotates)
|
||||
rootIds, err = workerAuthRepo.List(ctx, (*types.RootCertificate)(nil))
|
||||
require.NoError(err)
|
||||
assert.Len(rootIds, 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotateRootsJobFailure(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ctx := context.Background()
|
||||
wrapper := db.TestWrapper(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
|
||||
require.NoError(err)
|
||||
|
||||
got, err := newRotateRootsJob(ctx, &db.Db{}, &db.Db{}, kmsCache)
|
||||
require.NoError(err)
|
||||
|
||||
err = got.Run(ctx)
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func TestRegisterRotateRootsJob(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
ctx := context.Background()
|
||||
wrapper := db.TestWrapper(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
|
||||
require.NoError(err)
|
||||
|
||||
sched := scheduler.TestScheduler(t, conn, wrapper)
|
||||
|
||||
type args struct {
|
||||
s *scheduler.Scheduler
|
||||
w db.Writer
|
||||
r db.Reader
|
||||
kms *kms.Kms
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
options []servers.Option
|
||||
wantLimit int
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
}{
|
||||
{
|
||||
name: "nil scheduler",
|
||||
args: args{
|
||||
w: rw,
|
||||
r: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil writer",
|
||||
args: args{
|
||||
s: sched,
|
||||
r: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil reader",
|
||||
args: args{
|
||||
s: sched,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil kms",
|
||||
args: args{
|
||||
s: sched,
|
||||
w: rw,
|
||||
r: rw,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
s: sched,
|
||||
w: rw,
|
||||
r: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
wantLimit: db.DefaultLimit,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := RegisterJobs(ctx, tt.args.s, tt.args.r, tt.args.w, tt.args.kms)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "Unexpected error %s", err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/nodeenrollment"
|
||||
"github.com/hashicorp/nodeenrollment/rotation"
|
||||
)
|
||||
|
||||
// RotateRoots is a domain service function that initiates a rotation of root certificates
|
||||
// via a call to the nodenenrollment RotateRootCertificates function
|
||||
// Accepts the nodeenrollment option, WithCertificateLifetime(time.Duration) to specify the lifetime
|
||||
// of the generated cert(s)
|
||||
func RotateRoots(ctx context.Context, workerAuthRepo *WorkerAuthRepositoryStorage, opt ...nodeenrollment.Option) error {
|
||||
const op = "servers.RotateRoots"
|
||||
if workerAuthRepo == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing workerAuthRepo")
|
||||
}
|
||||
|
||||
_, err := rotation.RotateRootCertificates(ctx, workerAuthRepo, opt...)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
"github.com/hashicorp/nodeenrollment"
|
||||
"github.com/hashicorp/nodeenrollment/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRotateRoots(t *testing.T) {
|
||||
require, assert := require.New(t), assert.New(t)
|
||||
ctx := context.Background()
|
||||
wrapper := db.TestWrapper(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
|
||||
require.NoError(err)
|
||||
|
||||
rw := db.New(conn)
|
||||
workerAuthRepo, err := NewRepositoryStorage(ctx, rw, rw, kmsCache)
|
||||
require.NoError(err)
|
||||
|
||||
// Check that we have no roots first
|
||||
rootIds, err := workerAuthRepo.List(ctx, (*types.RootCertificate)(nil))
|
||||
require.NoError(err)
|
||||
assert.Len(rootIds, 0)
|
||||
|
||||
// Generate roots
|
||||
err = RotateRoots(ctx, workerAuthRepo, nodeenrollment.WithCertificateLifetime(time.Second*5))
|
||||
require.NoError(err)
|
||||
|
||||
// Check that we have roots now
|
||||
rootIds, err = workerAuthRepo.List(ctx, (*types.RootCertificate)(nil))
|
||||
require.NoError(err)
|
||||
assert.Len(rootIds, 2)
|
||||
certAuthority := &types.RootCertificates{Id: ca_id}
|
||||
err = workerAuthRepo.Load(ctx, certAuthority)
|
||||
require.NoError(err)
|
||||
require.NotNil(certAuthority.GetNext())
|
||||
require.NotNil(certAuthority.GetCurrent())
|
||||
|
||||
initialNext := certAuthority.GetNext()
|
||||
initialCurrent := certAuthority.GetCurrent()
|
||||
|
||||
// Rotate roots and assert that they've rotated
|
||||
err = RotateRoots(ctx, workerAuthRepo, nodeenrollment.WithCertificateLifetime(time.Second*5))
|
||||
require.NoError(err)
|
||||
|
||||
certAuthority2 := &types.RootCertificates{Id: ca_id}
|
||||
err = workerAuthRepo.Load(ctx, certAuthority2)
|
||||
require.NoError(err)
|
||||
require.NotNil(certAuthority2.GetNext())
|
||||
require.NotNil(certAuthority2.GetCurrent())
|
||||
rotatedNext := certAuthority2.GetNext()
|
||||
rotatedCurrent := certAuthority2.GetCurrent()
|
||||
|
||||
// Next and current should have changed
|
||||
assert.NotEqual(initialNext, rotatedNext)
|
||||
assert.NotEqual(initialCurrent, rotatedCurrent)
|
||||
|
||||
// And the old next root should now be current
|
||||
assert.Equal(initialNext, rotatedCurrent)
|
||||
}
|
||||
|
||||
func TestRotateRootsFailure(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ctx := context.Background()
|
||||
wrapper := db.TestWrapper(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
err := kmsCache.CreateKeys(context.Background(), scope.Global.String(), kms.WithRandomReader(rand.Reader))
|
||||
require.NoError(err)
|
||||
|
||||
workerAuthRepo, err := NewRepositoryStorage(ctx, &db.Db{}, &db.Db{}, kmsCache)
|
||||
require.NoError(err)
|
||||
|
||||
err = RotateRoots(ctx, workerAuthRepo)
|
||||
require.Error(err)
|
||||
}
|
||||
Reference in new issue
Block a user