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