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.
boundary/internal/daemon/worker/auth_rotation_test.go

167 lines
5.8 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package worker_test
import (
"bytes"
"context"
"sync/atomic"
"testing"
"time"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/daemon/controller"
"github.com/hashicorp/boundary/internal/daemon/worker"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/server/store"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nodeenrollment"
"github.com/hashicorp/nodeenrollment/types"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
// TestRotationTicking ensures that we see new credential information for a
// worker on both the controller side and worker side in a periodic fashion
func TestRotationTicking(t *testing.T) {
require, assert := require.New(t), assert.New(t)
logger := hclog.New(&hclog.LoggerOptions{
Level: hclog.Trace,
})
const rotationPeriod = 20 * time.Second
conf, err := config.DevController()
require.NoError(err)
workerAuthDebugEnabled := new(atomic.Bool)
workerAuthDebugEnabled.Store(true)
c := controller.NewTestController(t, &controller.TestControllerOpts{
Config: conf,
Logger: logger.Named("controller"),
WorkerAuthCaCertificateLifetime: rotationPeriod,
WorkerAuthDebuggingEnabled: workerAuthDebugEnabled,
})
// names should not be set when using pki workers
wConf, err := config.DevWorker()
require.NoError(err)
wConf.Worker.Name = ""
wConf.Worker.InitialUpstreams = c.ClusterAddrs()
w := worker.NewTestWorker(t, &worker.TestWorkerOpts{
InitialUpstreams: c.ClusterAddrs(),
Logger: logger.Named("worker"),
Config: wConf,
WorkerAuthDebuggingEnabled: workerAuthDebugEnabled,
})
// Get a server repo and worker auth repo
serversRepo, err := c.Controller().ServersRepoFn()
require.NoError(err)
workerAuthRepo, err := c.Controller().WorkerAuthRepoStorageFn()
require.NoError(err)
// Give time for the job to ensure that roots are created and offset us from
// the rotation period
time.Sleep(rotationPeriod / 2)
// Verify that there are no nodes authorized yet
auths, err := workerAuthRepo.List(c.Context(), (*types.NodeInformation)(nil))
require.NoError(err)
assert.Len(auths, 0)
// Perform initial authentication of worker to controller
reqBytes, err := base58.FastBase58Decoding(w.Worker().WorkerAuthRegistrationRequest)
require.NoError(err)
// Decode the proto into the request and create the worker
req := new(types.FetchNodeCredentialsRequest)
require.NoError(proto.Unmarshal(reqBytes, req))
newWorker, err := serversRepo.CreateWorker(c.Context(), &server.Worker{
Worker: &store.Worker{
ScopeId: scope.Global.String(),
},
}, server.WithFetchNodeCredentialsRequest(req))
require.NoError(err)
// Wait for a short while; there will be an initial rotation of credentials
// after authentication
time.Sleep(rotationPeriod / 2)
// Verify we see authorized credentials now
auths, err = workerAuthRepo.List(c.Context(), (*types.NodeInformation)(nil))
require.NoError(err)
assert.Len(auths, 2)
// Fetch creds and store current key
currNodeCreds, err := types.LoadNodeCredentials(
w.Context(),
w.Worker().WorkerAuthStorage,
nodeenrollment.CurrentId,
nodeenrollment.WithStorageWrapper(w.Config().WorkerAuthStorageKms),
)
require.NoError(err)
currKey := currNodeCreds.CertificatePublicKeyPkix
// Now we wait and check that we see new values in the DB and different
// creds on the worker after each rotation period
// Make sure we have a failsafe in case the below loop never finds what it's
// looking for; at expiration the List will fail and we'll hit the Require,
// exiting
deadlineCtx, deadlineCtxCancel := context.WithTimeout(c.Context(), 10*time.Minute)
defer deadlineCtxCancel()
rotationCount := 2
for {
if rotationCount > 5 {
break
}
nextRotation := w.Worker().AuthRotationNextRotation.Load()
time.Sleep((*nextRotation).Sub(time.Now()) + 5*time.Second)
// Verify we see 2- after credentials have rotated, we should see current and previous
auths, err = workerAuthRepo.List(deadlineCtx, (*types.NodeInformation)(nil))
require.NoError(err)
assert.Len(auths, 2)
// Fetch creds and compare current key
currNodeCreds, err := types.LoadNodeCredentials(
w.Context(),
w.Worker().WorkerAuthStorage,
nodeenrollment.CurrentId,
nodeenrollment.WithStorageWrapper(w.Config().WorkerAuthStorageKms),
)
require.NoError(err)
if bytes.Equal(currKey, currNodeCreds.CertificatePublicKeyPkix) {
// Load due to parallel tests may mean that we didn't hit rotation
// yet, loop back to evaluate again
continue
}
assert.NotEqual(currKey, currNodeCreds.CertificatePublicKeyPkix)
currKey = currNodeCreds.CertificatePublicKeyPkix
currKeyId, err := nodeenrollment.KeyIdFromPkix(currNodeCreds.CertificatePublicKeyPkix)
require.NoError(err)
assert.Equal(currKeyId, w.Worker().WorkerAuthCurrentKeyId.Load())
// Get workerAuthSet for this worker id and compare keys
workerAuthSet, err := workerAuthRepo.FindWorkerAuthByWorkerId(c.Context(), newWorker.PublicId)
require.NoError(err)
assert.NotNil(workerAuthSet)
assert.NotNil(workerAuthSet.Previous)
assert.NotNil(workerAuthSet.Current)
assert.Equal(workerAuthSet.Current.WorkerKeyIdentifier, currKeyId)
// Stop and start the client connections to ensure the new credentials
// are valid; if not, we won't establish a new connection and rotation
// will fail
require.NotNil(w.Worker().GrpcClientConn.Load())
require.NoError(w.Worker().GrpcClientConn.Load().Close())
require.NoError(w.Worker().StartControllerConnections())
rotationCount++
}
}