m/n/core: save owner public key in etcd
This is an early implementation of storing user credentials. It
currently does not support more then the owner credentials.
These are not yet used anywhere, but will be in a follow-up CL.
Change-Id: Ib876f7aaff44531dcae5a27875a960aaa9ec029f
Reviewed-on: https://review.monogon.dev/c/monogon/+/287
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/cluster/cluster_bootstrap.go b/metropolis/node/core/cluster/cluster_bootstrap.go
index 16253bf..2905d17 100644
--- a/metropolis/node/core/cluster/cluster_bootstrap.go
+++ b/metropolis/node/core/cluster/cluster_bootstrap.go
@@ -37,6 +37,8 @@
state, unlock := m.lock()
defer unlock()
+ ownerKey := bootstrap.OwnerPublicKey
+
state.configuration = &ppb.SealedConfiguration{}
// Mount new storage with generated CUK, and save LUK into sealed config proto.
@@ -112,8 +114,8 @@
return fmt.Errorf("when retrieving consensus user for curator: %w", err)
}
- if err := node.BootstrapStore(ctx, ckv); err != nil {
- return fmt.Errorf("failed to store new node in etcd: %w", err)
+ if err := curator.BootstrapFinish(ctx, ckv, &node, ownerKey); err != nil {
+ return fmt.Errorf("failed to finish bootstrap: %w", err)
}
// And short-circuit creating the curator CA and node certificate.
diff --git a/metropolis/node/core/curator/bootstrap.go b/metropolis/node/core/curator/bootstrap.go
index c764104..af0a038 100644
--- a/metropolis/node/core/curator/bootstrap.go
+++ b/metropolis/node/core/curator/bootstrap.go
@@ -9,6 +9,7 @@
"google.golang.org/protobuf/proto"
"source.monogon.dev/metropolis/node/core/consensus/client"
+ ppb "source.monogon.dev/metropolis/node/core/curator/proto/private"
"source.monogon.dev/metropolis/pkg/pki"
)
@@ -50,33 +51,49 @@
return
}
-// BootstrapStore saves the Node into etcd, without regard for any other cluster
-// state and directly using a given etcd client.
+const (
+ initialOwnerEtcdPath = "/global/initial_owner"
+)
+
+// BootstrapFinish saves the given Node and initial cluster owner pubkey into
+// etcd, without regard for any other cluster state and directly using a given
+// etcd client.
//
-// This can only be used by the cluster bootstrap logic.
-func (n *Node) BootstrapStore(ctx context.Context, etcd client.Namespaced) error {
- // Currently the only flow to store a node to etcd is a write-once flow:
- // once a node is created, it cannot be deleted or updated. In the future,
- // flows to change cluster node roles might be introduced (ie. to promote
- // nodes to consensus members, etc).
- key := n.etcdPath()
- msg := n.proto()
- nodeRaw, err := proto.Marshal(msg)
+// This is ran by the cluster bootstrap workflow to finish bootstrapping a
+// cluster - afterwards, this cluster will be ready to serve.
+//
+// This can only be used by the cluster bootstrap logic, and may only be called
+// once. It's guaranteed to either succeed fully or fail fully, without leaving
+// the cluster in an inconsistent state.
+func BootstrapFinish(ctx context.Context, etcd client.Namespaced, initialNode *Node, pubkey []byte) error {
+ nodeKey := initialNode.etcdPath()
+ nodeRaw, err := proto.Marshal(initialNode.proto())
if err != nil {
return fmt.Errorf("failed to marshal node: %w", err)
}
+ owner := &ppb.InitialOwner{
+ PublicKey: pubkey,
+ }
+ ownerKey := initialOwnerEtcdPath
+ ownerRaw, err := proto.Marshal(owner)
+ if err != nil {
+ return fmt.Errorf("failed to marshal iniail owner: %w", err)
+ }
+
res, err := etcd.Txn(ctx).If(
- clientv3.Compare(clientv3.CreateRevision(key), "=", 0),
+ clientv3.Compare(clientv3.CreateRevision(nodeKey), "=", 0),
+ clientv3.Compare(clientv3.CreateRevision(ownerKey), "=", 0),
).Then(
- clientv3.OpPut(key, string(nodeRaw)),
+ clientv3.OpPut(nodeKey, string(nodeRaw)),
+ clientv3.OpPut(ownerKey, string(ownerRaw)),
).Commit()
if err != nil {
- return fmt.Errorf("failed to store node: %w", err)
+ return fmt.Errorf("failed to store initial cluster state: %w", err)
}
if !res.Succeeded {
- return fmt.Errorf("attempted to re-register node (unsupported flow)")
+ return fmt.Errorf("cluster already bootstrapped")
}
return nil
}
diff --git a/metropolis/node/core/curator/proto/private/storage.proto b/metropolis/node/core/curator/proto/private/storage.proto
index 5ad01c4..6f42d3c 100644
--- a/metropolis/node/core/curator/proto/private/storage.proto
+++ b/metropolis/node/core/curator/proto/private/storage.proto
@@ -6,6 +6,9 @@
// Node describes a single node's state in etcd. This is only ever visible to
// the curator, and fully managed by the curator.
+//
+// Serialized nodes are stored in /nodes/$id, where $id is the node's ID as
+// calculated from its public key.
message Node {
// The node's public key.
bytes public_key = 1;
@@ -22,3 +25,14 @@
// The node's intended roles when running.
metropolis.proto.common.NodeRoles roles = 4;
}
+
+// Information about the cluster owner, currently the only Metropolis management
+// entity, named 'owner' in public APIs.
+//
+// In the future, once we have implemented a manager/user entity system, this
+// will be replaced by a proper per-user entry.
+//
+// Stored under /global/initial_owner.
+message InitialOwner {
+ bytes public_key = 1;
+}