Skip to content

Commit

Permalink
keyring state store operations (#13016)
Browse files Browse the repository at this point in the history
Implement the basic upsert, list, and delete operations for
`RootKeyMeta` needed by the Keyring RPCs.

This changeset also implements two convenience methods
`RootKeyMetaByID` and `GetActiveRootKeyMeta` which are useful for
testing but also will be needed to implement the rest of the RPCs.
  • Loading branch information
tgross committed May 19, 2022
1 parent ae1457c commit 8473c30
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 3 deletions.
121 changes: 119 additions & 2 deletions nomad/state/state_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6627,14 +6627,92 @@ func (s *StateStore) SecureVariablesQuotas(ws memdb.WatchSet) (memdb.ResultItera
return iter, nil
}

// UpsertRootKeyMeta saves root key meta or updates it in-place.
func (s *StateStore) UpsertRootKeyMeta(index uint64, rootKeyMeta *structs.RootKeyMeta) error {
return nil
txn := s.db.WriteTxn(index)
defer txn.Abort()

// get any existing key for updating
raw, err := txn.First(TableRootKeyMeta, indexID, rootKeyMeta.KeyID)
if err != nil {
return fmt.Errorf("root key metadata lookup failed: %v", err)
}

isRotation := false

if raw != nil {
existing := raw.(*structs.RootKeyMeta)
rootKeyMeta.CreateIndex = existing.CreateIndex
rootKeyMeta.CreateTime = existing.CreateTime
isRotation = !existing.Active && rootKeyMeta.Active
} else {
rootKeyMeta.CreateIndex = index
rootKeyMeta.CreateTime = time.Now()
isRotation = rootKeyMeta.Active
}
rootKeyMeta.ModifyIndex = index

// if the upsert is for a newly-active key, we need to set all the
// other keys as inactive in the same transaction.
if isRotation {
iter, err := txn.Get(TableRootKeyMeta, indexID)
if err != nil {
return err
}
for {
raw := iter.Next()
if raw == nil {
break
}
key := raw.(*structs.RootKeyMeta)
if key.Active {
key.Active = false
key.ModifyIndex = index
if err := txn.Insert(TableRootKeyMeta, key); err != nil {
return err
}
}
}
}

if err := txn.Insert(TableRootKeyMeta, rootKeyMeta); err != nil {
return err
}

// update the indexes table
if err := txn.Insert("index", &IndexEntry{TableRootKeyMeta, index}); err != nil {
return fmt.Errorf("index update failed: %v", err)
}
return txn.Commit()
}

// DeleteRootKeyMeta deletes a single root key, or returns an error if
// it doesn't exist.
func (s *StateStore) DeleteRootKeyMeta(index uint64, keyID string) error {
return nil
txn := s.db.WriteTxn(index)
defer txn.Abort()

// find the old key
existing, err := txn.First(TableRootKeyMeta, indexID, keyID)
if err != nil {
return fmt.Errorf("root key metadata lookup failed: %v", err)
}
if existing == nil {
return fmt.Errorf("root key metadata not found")
}
if err := txn.Delete(TableRootKeyMeta, existing); err != nil {
return fmt.Errorf("root key metadata delete failed: %v", err)
}

// update the indexes table
if err := txn.Insert("index", &IndexEntry{TableRootKeyMeta, index}); err != nil {
return fmt.Errorf("index update failed: %v", err)
}

return txn.Commit()
}

// RootKeyMetas returns an iterator over all root key metadata
func (s *StateStore) RootKeyMetas(ws memdb.WatchSet) (memdb.ResultIterator, error) {
txn := s.db.ReadTxn()

Expand All @@ -6646,3 +6724,42 @@ func (s *StateStore) RootKeyMetas(ws memdb.WatchSet) (memdb.ResultIterator, erro
ws.Add(iter.WatchCh())
return iter, nil
}

// RootKeyMetaByID returns a specific root key meta
func (s *StateStore) RootKeyMetaByID(ws memdb.WatchSet, id string) (*structs.RootKeyMeta, error) {
txn := s.db.ReadTxn()

watchCh, raw, err := txn.FirstWatch(TableRootKeyMeta, indexID, id)
if err != nil {
return nil, fmt.Errorf("root key metadata lookup failed: %v", err)
}
ws.Add(watchCh)

if raw != nil {
return raw.(*structs.RootKeyMeta), nil
}
return nil, nil
}

// GetActiveRootKeyMeta returns the metadata for the currently active root key
func (s *StateStore) GetActiveRootKeyMeta(ws memdb.WatchSet) (*structs.RootKeyMeta, error) {
txn := s.db.ReadTxn()

iter, err := txn.Get(TableRootKeyMeta, indexID)
if err != nil {
return nil, err
}
ws.Add(iter.WatchCh())

for {
raw := iter.Next()
if raw == nil {
break
}
key := raw.(*structs.RootKeyMeta)
if key.Active {
return key, nil
}
}
return nil, nil
}
69 changes: 69 additions & 0 deletions nomad/state/state_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9852,6 +9852,75 @@ func TestStateStore_UpsertScalingEvent_LimitAndOrder(t *testing.T) {
require.Equal(expectedEvents, actualEvents)
}

func TestStateStore_RootKeyMetaData_CRUD(t *testing.T) {
ci.Parallel(t)
store := testStateStore(t)
index, err := store.LatestIndex()
require.NoError(t, err)

// create 3 default keys, one of which is active
keyIDs := []string{}
for i := 0; i < 3; i++ {
key := structs.NewRootKeyMeta()
keyIDs = append(keyIDs, key.KeyID)
if i == 0 {
key.Active = true
}
index++
require.NoError(t, store.UpsertRootKeyMeta(index, key))
}

// retrieve the active key
activeKey, err := store.GetActiveRootKeyMeta(nil)
require.NoError(t, err)
require.NotNil(t, activeKey)

// update an inactive key to active and verify the rotation
inactiveKey, err := store.RootKeyMetaByID(nil, keyIDs[1])
require.NoError(t, err)
require.NotNil(t, inactiveKey)
oldCreateIndex := inactiveKey.CreateIndex
newlyActiveKey := inactiveKey.Copy()
newlyActiveKey.Active = true
index++
require.NoError(t, store.UpsertRootKeyMeta(index, newlyActiveKey))

iter, err := store.RootKeyMetas(nil)
require.NoError(t, err)
for {
raw := iter.Next()
if raw == nil {
break
}
key := raw.(*structs.RootKeyMeta)
if key.KeyID == newlyActiveKey.KeyID {
require.True(t, key.Active, "expected updated key to be active")
require.Equal(t, oldCreateIndex, key.CreateIndex)
} else {
require.False(t, key.Active, "expected other keys to be inactive")
}
}

// delete the active key and verify it's been deleted
index++
require.NoError(t, store.DeleteRootKeyMeta(index, keyIDs[1]))

iter, err = store.RootKeyMetas(nil)
require.NoError(t, err)
var found int
for {
raw := iter.Next()
if raw == nil {
break
}
key := raw.(*structs.RootKeyMeta)
require.NotEqual(t, keyIDs[1], key.KeyID)
require.False(t, key.Active, "expected remaining keys to be inactive")
found++
}
require.Equal(t, 2, found, "expected only 2 keys remaining")
}

func TestStateStore_Abandon(t *testing.T) {
ci.Parallel(t)

Expand Down
23 changes: 22 additions & 1 deletion nomad/structs/secure_variables.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package structs

import "time"
import (
"time"

"github.com/hashicorp/nomad/helper/uuid"
)

// SecureVariable is the metadata envelope for a Secure Variable
type SecureVariable struct {
Expand Down Expand Up @@ -151,6 +155,23 @@ type RootKeyMeta struct {
ModifyIndex uint64
}

// NewRootKeyMeta returns a new RootKeyMeta with default values
func NewRootKeyMeta() *RootKeyMeta {
return &RootKeyMeta{
KeyID: uuid.Generate(),
Algorithm: EncryptionAlgorithmXChaCha20,
CreateTime: time.Now(),
}
}

func (rkm *RootKeyMeta) Copy() *RootKeyMeta {
if rkm == nil {
return nil
}
out := *rkm
return &out
}

// EncryptionAlgorithm chooses which algorithm is used for
// encrypting / decrypting entries with this key
type EncryptionAlgorithm string
Expand Down

0 comments on commit 8473c30

Please sign in to comment.