Merge pull request #2130 from hashicorp/irindos-rotate-roots

feature(WorkerAuth): Create RotateRoots domain and job
This commit is contained in:
Irena Rindos authored and GitHub committed 2022-06-06 14:50:21 -04:00
commit cc232b743e
6 files changed
+472 -10

No files matched your search

+4 -10
View File
@@ -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
}
+50
View File
@@ -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
}
+82
View File
@@ -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)
})
}
}
+27
View File
@@ -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)
}