open-nomad/nomad/keyring_endpoint_test.go

298 lines
8.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package nomad
import (
"sync"
"testing"
"time"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc/v2"
"github.com/stretchr/testify/require"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
)
// TestKeyringEndpoint_CRUD exercises the basic keyring operations
func TestKeyringEndpoint_CRUD(t *testing.T) {
ci.Parallel(t)
srv, rootToken, shutdown := TestACLServer(t, func(c *Config) {
c.NumSchedulers = 0 // Prevent automatic dequeue
})
defer shutdown()
testutil.WaitForLeader(t, srv.RPC)
codec := rpcClient(t, srv)
// Upsert a new key
key, err := structs.NewRootKey(structs.EncryptionAlgorithmAES256GCM)
require.NoError(t, err)
id := key.Meta.KeyID
key.Meta.SetActive()
updateReq := &structs.KeyringUpdateRootKeyRequest{
RootKey: key,
WriteRequest: structs.WriteRequest{Region: "global"},
}
var updateResp structs.KeyringUpdateRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
updateReq.AuthToken = rootToken.SecretID
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
require.NotEqual(t, uint64(0), updateResp.Index)
// Get and List don't need a token here because they rely on mTLS role verification
getReq := &structs.KeyringGetRootKeyRequest{
KeyID: id,
QueryOptions: structs.QueryOptions{Region: "global"},
}
var getResp structs.KeyringGetRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Get", getReq, &getResp)
require.NoError(t, err)
require.Equal(t, updateResp.Index, getResp.Index)
require.Equal(t, structs.EncryptionAlgorithmAES256GCM, getResp.Key.Meta.Algorithm)
// Make a blocking query for List and wait for an Update. Note
// that List/Get queries don't need ACL tokens in the test server
// because they always pass the mTLS check
var wg sync.WaitGroup
wg.Add(1)
var listResp structs.KeyringListRootKeyMetaResponse
go func() {
defer wg.Done()
codec := rpcClient(t, srv) // not safe to share across goroutines
listReq := &structs.KeyringListRootKeyMetaRequest{
QueryOptions: structs.QueryOptions{
Region: "global",
MinQueryIndex: getResp.Index,
},
}
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
require.NoError(t, err)
}()
updateReq.RootKey.Meta.CreateTime = time.Now().UTC().UnixNano()
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
require.NotEqual(t, uint64(0), updateResp.Index)
// wait for the blocking query to complete and check the response
wg.Wait()
require.Equal(t, listResp.Index, updateResp.Index)
require.Len(t, listResp.Keys, 2) // bootstrap + new one
// Delete the key and verify that it's gone
delReq := &structs.KeyringDeleteRootKeyRequest{
KeyID: id,
WriteRequest: structs.WriteRequest{Region: "global"},
}
var delResp structs.KeyringDeleteRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
delReq.AuthToken = rootToken.SecretID
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
require.EqualError(t, err, "active root key cannot be deleted - call rotate first")
// set inactive
updateReq.RootKey.Meta.SetInactive()
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
require.NoError(t, err)
require.Greater(t, delResp.Index, getResp.Index)
listReq := &structs.KeyringListRootKeyMetaRequest{
QueryOptions: structs.QueryOptions{Region: "global"},
}
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
require.NoError(t, err)
require.Greater(t, listResp.Index, getResp.Index)
require.Len(t, listResp.Keys, 1) // just the bootstrap key
}
// TestKeyringEndpoint_validateUpdate exercises all the various
// validations we make for the update RPC
func TestKeyringEndpoint_InvalidUpdates(t *testing.T) {
ci.Parallel(t)
srv, rootToken, shutdown := TestACLServer(t, func(c *Config) {
c.NumSchedulers = 0 // Prevent automatic dequeue
})
defer shutdown()
testutil.WaitForLeader(t, srv.RPC)
codec := rpcClient(t, srv)
// Setup an existing key
key, err := structs.NewRootKey(structs.EncryptionAlgorithmAES256GCM)
require.NoError(t, err)
id := key.Meta.KeyID
key.Meta.SetActive()
updateReq := &structs.KeyringUpdateRootKeyRequest{
RootKey: key,
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: rootToken.SecretID,
},
}
var updateResp structs.KeyringUpdateRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
testCases := []struct {
key *structs.RootKey
expectedErrMsg string
}{
{
key: &structs.RootKey{},
expectedErrMsg: "root key metadata is required",
},
{
key: &structs.RootKey{Meta: &structs.RootKeyMeta{}},
expectedErrMsg: "root key UUID is required",
},
{
key: &structs.RootKey{Meta: &structs.RootKeyMeta{KeyID: "invalid"}},
expectedErrMsg: "root key UUID is required",
},
{
key: &structs.RootKey{Meta: &structs.RootKeyMeta{
KeyID: id,
Algorithm: structs.EncryptionAlgorithmAES256GCM,
}},
expectedErrMsg: "root key state \"\" is invalid",
},
{
key: &structs.RootKey{Meta: &structs.RootKeyMeta{
KeyID: id,
Algorithm: structs.EncryptionAlgorithmAES256GCM,
State: structs.RootKeyStateActive,
}},
expectedErrMsg: "root key material is required",
},
{
key: &structs.RootKey{
Key: []byte{0x01},
Meta: &structs.RootKeyMeta{
KeyID: id,
Algorithm: "whatever",
State: structs.RootKeyStateActive,
}},
expectedErrMsg: "root key algorithm cannot be changed after a key is created",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.expectedErrMsg, func(t *testing.T) {
updateReq := &structs.KeyringUpdateRootKeyRequest{
RootKey: tc.key,
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: rootToken.SecretID,
},
}
var updateResp structs.KeyringUpdateRootKeyResponse
err := msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.EqualError(t, err, tc.expectedErrMsg)
})
}
}
// TestKeyringEndpoint_Rotate exercises the key rotation logic
func TestKeyringEndpoint_Rotate(t *testing.T) {
ci.Parallel(t)
srv, rootToken, shutdown := TestACLServer(t, func(c *Config) {
c.NumSchedulers = 0 // Prevent automatic dequeue
})
defer shutdown()
testutil.WaitForLeader(t, srv.RPC)
codec := rpcClient(t, srv)
// Setup an existing key
key, err := structs.NewRootKey(structs.EncryptionAlgorithmAES256GCM)
require.NoError(t, err)
key.Meta.SetActive()
updateReq := &structs.KeyringUpdateRootKeyRequest{
RootKey: key,
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: rootToken.SecretID,
},
}
var updateResp structs.KeyringUpdateRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
// Rotate the key
rotateReq := &structs.KeyringRotateRootKeyRequest{
WriteRequest: structs.WriteRequest{
Region: "global",
},
}
var rotateResp structs.KeyringRotateRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Rotate", rotateReq, &rotateResp)
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
rotateReq.AuthToken = rootToken.SecretID
err = msgpackrpc.CallWithCodec(codec, "Keyring.Rotate", rotateReq, &rotateResp)
require.NoError(t, err)
require.NotEqual(t, updateResp.Index, rotateResp.Index)
newID := rotateResp.Key.KeyID
// Verify we have a new key and the old one is inactive
listReq := &structs.KeyringListRootKeyMetaRequest{
QueryOptions: structs.QueryOptions{
Region: "global",
},
}
var listResp structs.KeyringListRootKeyMetaResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
require.NoError(t, err)
require.Greater(t, listResp.Index, updateResp.Index)
require.Len(t, listResp.Keys, 3) // bootstrap + old + new
for _, keyMeta := range listResp.Keys {
if keyMeta.KeyID != newID {
require.False(t, keyMeta.Active(), "expected old keys to be inactive")
} else {
require.True(t, keyMeta.Active(), "expected new key to be inactive")
}
}
getReq := &structs.KeyringGetRootKeyRequest{
KeyID: newID,
QueryOptions: structs.QueryOptions{
Region: "global",
},
}
var getResp structs.KeyringGetRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Get", getReq, &getResp)
require.NoError(t, err)
gotKey := getResp.Key
require.Len(t, gotKey.Key, 32)
}