m/n/c/curator: listen on public gRPC
This enables listening on CuratorPort (which was called
NodeServicePort) using TLS node certificates. No service is yet running
on the new gRPC listener.
Change-Id: I436ac1ae9cbdb257419ad114262fc2a7516396b1
Reviewed-on: https://review.monogon.dev/c/monogon/+/288
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/cluster/BUILD.bazel b/metropolis/node/core/cluster/BUILD.bazel
index 322a337..a68f63c 100644
--- a/metropolis/node/core/cluster/BUILD.bazel
+++ b/metropolis/node/core/cluster/BUILD.bazel
@@ -23,6 +23,7 @@
"//metropolis/proto/api:go_default_library",
"//metropolis/proto/common:go_default_library",
"//metropolis/proto/private:go_default_library",
+ "@org_golang_google_grpc//credentials:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
],
)
diff --git a/metropolis/node/core/cluster/node.go b/metropolis/node/core/cluster/node.go
index 0d4daac..0e3c29a 100644
--- a/metropolis/node/core/cluster/node.go
+++ b/metropolis/node/core/cluster/node.go
@@ -3,9 +3,12 @@
import (
"crypto/ed25519"
"crypto/subtle"
+ "crypto/tls"
"crypto/x509"
"fmt"
+ "google.golang.org/grpc/credentials"
+
"source.monogon.dev/metropolis/node/core/curator"
"source.monogon.dev/metropolis/node/core/localstorage"
)
@@ -138,3 +141,22 @@
func (nc *NodeCertificate) ID() string {
return curator.NodeID(nc.PublicKey())
}
+
+// PublicGRPCServerCredentials returns gRPC TransportCredentials that should be
+// used by this node to run public gRPC services (ie. the AAA service and any
+// other management/user services).
+//
+// SECURITY: The returned TransportCredentials accepts _any_ client certificate
+// served by the client and does not perform any verification. The gRPC service
+// instance (via per-method checks or middleware) should perform user
+// authentication/authorization.
+func (nc *NodeCredentials) PublicGRPCServerCredentials() credentials.TransportCredentials {
+ tlsCert := tls.Certificate{
+ Certificate: [][]byte{nc.node.Raw},
+ PrivateKey: nc.private,
+ }
+ return credentials.NewTLS(&tls.Config{
+ Certificates: []tls.Certificate{tlsCert},
+ ClientAuth: tls.RequireAnyClientCert,
+ })
+}
diff --git a/metropolis/node/core/curator/BUILD.bazel b/metropolis/node/core/curator/BUILD.bazel
index 594989d..0374297 100644
--- a/metropolis/node/core/curator/BUILD.bazel
+++ b/metropolis/node/core/curator/BUILD.bazel
@@ -15,6 +15,7 @@
importpath = "source.monogon.dev/metropolis/node/core/curator",
visibility = ["//visibility:public"],
deps = [
+ "//metropolis/node:go_default_library",
"//metropolis/node/core/consensus/client:go_default_library",
"//metropolis/node/core/curator/proto/api:go_default_library",
"//metropolis/node/core/curator/proto/private:go_default_library",
@@ -30,6 +31,7 @@
"@io_etcd_go_etcd//clientv3/concurrency:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//codes:go_default_library",
+ "@org_golang_google_grpc//credentials:go_default_library",
"@org_golang_google_grpc//status:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
diff --git a/metropolis/node/core/curator/curator.go b/metropolis/node/core/curator/curator.go
index a872c20..c0e224f 100644
--- a/metropolis/node/core/curator/curator.go
+++ b/metropolis/node/core/curator/curator.go
@@ -19,6 +19,7 @@
"go.etcd.io/etcd/clientv3/concurrency"
"google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
"google.golang.org/protobuf/proto"
"source.monogon.dev/metropolis/node/core/consensus/client"
@@ -46,7 +47,8 @@
LeaderTTL time.Duration
// Directory is the curator ephemeral directory in which the curator will
// store its local domain socket for connections from the node.
- Directory *localstorage.EphemeralCuratorDirectory
+ Directory *localstorage.EphemeralCuratorDirectory
+ ServerCredentials credentials.TransportCredentials
}
// Service is the Curator service. See the package-level documentation for more
@@ -259,6 +261,7 @@
// running leader, or forwarding to a remotely running leader.
lis := listener{
directory: s.config.Directory,
+ publicCreds: s.config.ServerCredentials,
electionWatch: s.electionWatch,
etcd: s.config.Etcd,
dispatchC: make(chan dispatchRequest),
diff --git a/metropolis/node/core/curator/listener.go b/metropolis/node/core/curator/listener.go
index f2a76f5..b578290 100644
--- a/metropolis/node/core/curator/listener.go
+++ b/metropolis/node/core/curator/listener.go
@@ -8,8 +8,10 @@
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
+ "source.monogon.dev/metropolis/node"
"source.monogon.dev/metropolis/node/core/consensus/client"
cpb "source.monogon.dev/metropolis/node/core/curator/proto/api"
"source.monogon.dev/metropolis/node/core/localstorage"
@@ -40,7 +42,8 @@
etcd client.Namespaced
// directory is the ephemeral directory in which the local gRPC socket will
// be available for node-local consumers.
- directory *localstorage.EphemeralCuratorDirectory
+ directory *localstorage.EphemeralCuratorDirectory
+ publicCreds credentials.TransportCredentials
// electionWatch is a function that returns an active electionWatcher for the
// listener to use when determining local leadership. As the listener may
// restart on error, this factory-function is used instead of an electionWatcher
@@ -199,20 +202,29 @@
}
// TODO(q3k): recreate socket if already exists? Is this needed?
- lis, err := net.ListenUnix("unix", &net.UnixAddr{Name: l.directory.ClientSocket.FullPath(), Net: "unix"})
+ lisLocal, err := net.ListenUnix("unix", &net.UnixAddr{Name: l.directory.ClientSocket.FullPath(), Net: "unix"})
if err != nil {
- return fmt.Errorf("failed to listen on curator listener socket: %w", err)
+ return fmt.Errorf("failed to listen on local curator socket: %w", err)
+ }
+ lisPublic, err := net.Listen("tcp", fmt.Sprintf(":%d", node.CuratorServicePort))
+ if err != nil {
+ return fmt.Errorf("failed to listen on public curator socket: %w", err)
}
- // TODO(q3k): run remote/public gRPC listener.
+ srvLocal := grpc.NewServer()
+ srvPublic := grpc.NewServer(grpc.Creds(l.publicCreds))
- srv := grpc.NewServer()
- cpb.RegisterCuratorServer(srv, l)
+ cpb.RegisterCuratorServer(srvLocal, l)
+ // TODO(q3k): register servers on srvPublic.
- if err := supervisor.Run(ctx, "local", supervisor.GRPCServer(srv, lis, true)); err != nil {
+ if err := supervisor.Run(ctx, "local", supervisor.GRPCServer(srvLocal, lisLocal, true)); err != nil {
return fmt.Errorf("while starting local gRPC listener: %w", err)
}
- supervisor.Logger(ctx).Info("Listener running.")
+ if err := supervisor.Run(ctx, "public", supervisor.GRPCServer(srvPublic, lisPublic, true)); err != nil {
+ return fmt.Errorf("while starting public gRPC listener: %w", err)
+ }
+
+ supervisor.Logger(ctx).Info("Listeners running.")
supervisor.Signal(ctx, supervisor.SignalHealthy)
// Keep the listener running, as its a parent to the gRPC listener.
diff --git a/metropolis/node/core/main.go b/metropolis/node/core/main.go
index d665fc4..71f4227 100644
--- a/metropolis/node/core/main.go
+++ b/metropolis/node/core/main.go
@@ -170,6 +170,9 @@
return fmt.Errorf("failed to retrieve consensus kubernetes PKI client: %w", err)
}
+ // TODO(q3k): restart curator on credentials change?
+ curatorServerCreds := status.Credentials.PublicGRPCServerCredentials()
+
// Start cluster curator. The cluster curator is responsible for lifecycle
// management of the cluster.
// In the future, this will only be started on nodes that run etcd.
@@ -177,8 +180,9 @@
Etcd: ckv,
NodeID: status.Credentials.ID(),
// TODO(q3k): make this configurable?
- LeaderTTL: time.Second * 5,
- Directory: &root.Ephemeral.Curator,
+ LeaderTTL: time.Second * 5,
+ Directory: &root.Ephemeral.Curator,
+ ServerCredentials: curatorServerCreds,
})
if err := supervisor.Run(ctx, "curator", c.Run); err != nil {
close(trapdoor)
diff --git a/metropolis/node/ports.go b/metropolis/node/ports.go
index c63ec38..ed1c323 100644
--- a/metropolis/node/ports.go
+++ b/metropolis/node/ports.go
@@ -17,7 +17,7 @@
package node
const (
- NodeServicePort = 7835
+ CuratorServicePort = 7835
ConsensusPort = 7834
MasterServicePort = 7833
ExternalServicePort = 7836
diff --git a/metropolis/test/launch/launch.go b/metropolis/test/launch/launch.go
index f440adb..0d13792 100644
--- a/metropolis/test/launch/launch.go
+++ b/metropolis/test/launch/launch.go
@@ -141,7 +141,7 @@
}
// NodePorts is the list of ports a fully operational Metropolis node listens on
-var NodePorts = []uint16{node.ConsensusPort, node.NodeServicePort, node.MasterServicePort,
+var NodePorts = []uint16{node.ConsensusPort, node.CuratorServicePort, node.MasterServicePort,
node.ExternalServicePort, node.DebugServicePort, node.KubernetesAPIPort, node.DebuggerPort}
// IdentityPortMap returns a port map where each given port is mapped onto itself