diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index 3543e7cdf..0d7f57786 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -6664,14 +6664,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() @@ -6683,3 +6761,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 +} diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index 28edefcc2..512f0336f 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -9908,6 +9908,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) diff --git a/nomad/structs/secure_variables.go b/nomad/structs/secure_variables.go index a3fd63223..f87d6fe6b 100644 --- a/nomad/structs/secure_variables.go +++ b/nomad/structs/secure_variables.go @@ -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 { @@ -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