blob: 3d6917d2acf69256cda25465abb5dc4f33a06d0c [file] [log] [blame]
Serge Bazanskid7d6e022021-09-01 15:03:06 +02001package rpc
Serge Bazanski9ffa1f92021-09-01 15:42:23 +02002
3import (
4 "context"
5 "crypto/tls"
6 "crypto/x509"
7 "strings"
8
9 "google.golang.org/grpc"
10 "google.golang.org/grpc/codes"
11 "google.golang.org/grpc/credentials"
12 "google.golang.org/grpc/peer"
13 "google.golang.org/grpc/status"
14 "google.golang.org/protobuf/proto"
15 "google.golang.org/protobuf/reflect/protoreflect"
16 "google.golang.org/protobuf/reflect/protoregistry"
17
Serge Bazanskid7d6e022021-09-01 15:03:06 +020018 cpb "source.monogon.dev/metropolis/node/core/curator/proto/api"
Serge Bazanski9ffa1f92021-09-01 15:42:23 +020019 apb "source.monogon.dev/metropolis/proto/api"
20 epb "source.monogon.dev/metropolis/proto/ext"
21)
22
Serge Bazanskid7d6e022021-09-01 15:03:06 +020023// ClusterServices is the interface containing all gRPC services that a
24// Metropolis Cluster implements on its public interface. With the current
25// implementaiton of Metropolis, this is all implemented by the Curator.
26type ClusterServices interface {
27 cpb.CuratorServer
28 apb.AAAServer
29 apb.ManagementServer
Serge Bazanski9ffa1f92021-09-01 15:42:23 +020030}
31
Serge Bazanskid7d6e022021-09-01 15:03:06 +020032// ServerSecurity are the security options of a RPC server that will run
33// ClusterServices on a Metropolis node. It contains all the data for the
34// server implementation to authenticate itself to the clients and authenticate
35// and authorize clients connecting to it.
36type ServerSecurity struct {
37 // NodeCredentials is the TLS certificate/key of the node that the server
38 // implementation is running on. It should be signed by
39 // ClusterCACertificate.
40 NodeCredentials tls.Certificate
41 // ClusterCACertificate is the cluster's CA certificate. It will be used to
42 // authenticate the client certificates of incoming gRPC connections.
43 ClusterCACertificate *x509.Certificate
44}
45
46// SetupPublicGRPC returns a grpc.Server ready to listen and serve all public
47// gRPC APIs that the cluster server implementation should run, with all calls
48// being authenticated and authorized based on the data in ServerSecurity. The
49// argument 'impls' is the object implementing the gRPC APIs.
50//
51// This effectively configures gRPC interceptors that verify
52// metropolis.proto.ext.authorizaton options and authenticate/authorize
53// incoming connections. It also runs the gRPC server with the correct TLS
54// settings for authenticating itself to callers.
55func (l *ServerSecurity) SetupPublicGRPC(impls ClusterServices) *grpc.Server {
Serge Bazanski9ffa1f92021-09-01 15:42:23 +020056 publicCreds := credentials.NewTLS(&tls.Config{
Serge Bazanskid7d6e022021-09-01 15:03:06 +020057 Certificates: []tls.Certificate{l.NodeCredentials},
Serge Bazanski9ffa1f92021-09-01 15:42:23 +020058 ClientAuth: tls.RequestClientCert,
59 })
60
61 s := grpc.NewServer(
62 grpc.Creds(publicCreds),
63 grpc.UnaryInterceptor(l.unaryInterceptor),
64 grpc.StreamInterceptor(l.streamInterceptor),
65 )
Serge Bazanskid7d6e022021-09-01 15:03:06 +020066 cpb.RegisterCuratorServer(s, impls)
Serge Bazanski9ffa1f92021-09-01 15:42:23 +020067 apb.RegisterAAAServer(s, impls)
68 apb.RegisterManagementServer(s, impls)
69 return s
70}
71
72// authorize performs an authorization check for the given gRPC context
73// (containing peer information) and given RPC method name (as obtained from
74// FullMethodName in {Unary,Stream}ServerInfo). The actual authorization
75// requirements per method are retrieved from the Authorization protobuf
76// option applied to the RPC method.
77//
78// If the peer (as retrieved from the context) is authorized to run this method,
79// no error is returned. Otherwise, a gRPC status is returned outlining the
80// reason the authorization being rejected.
Serge Bazanskid7d6e022021-09-01 15:03:06 +020081func (l *ServerSecurity) authorize(ctx context.Context, methodName string) error {
Serge Bazanski9ffa1f92021-09-01 15:42:23 +020082 if !strings.HasPrefix(methodName, "/") {
83 return status.Errorf(codes.InvalidArgument, "invalid method name %q", methodName)
84 }
85 methodName = strings.ReplaceAll(methodName[1:], "/", ".")
86 desc, err := protoregistry.GlobalFiles.FindDescriptorByName(protoreflect.FullName(methodName))
87 if err != nil {
88 return status.Errorf(codes.InvalidArgument, "could not retrieve descriptor for method: %v", err)
89 }
90 method, ok := desc.(protoreflect.MethodDescriptor)
91 if !ok {
92 return status.Error(codes.InvalidArgument, "querying method name did not yield a MethodDescriptor")
93 }
94
95 // Get authorization extension, defaults to no options set.
96 authz, ok := proto.GetExtension(method.Options(), epb.E_Authorization).(*epb.Authorization)
97 if !ok || authz == nil {
98 authz = &epb.Authorization{}
99 }
100
101 // If unauthenticated connections are allowed, let them through immediately.
102 if authz.AllowUnauthenticated && len(authz.Need) == 0 {
103 return nil
104 }
105
106 // Otherwise, we check that the other side of the connection is authenticated
107 // using a valid cluster CA client certificate.
108 p, ok := peer.FromContext(ctx)
109 if !ok {
110 return status.Error(codes.Unavailable, "could not retrive peer info")
111 }
112 tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
113 if !ok {
114 return status.Error(codes.Unauthenticated, "connection not secure")
115 }
116 count := len(tlsInfo.State.PeerCertificates)
117 if count == 0 {
118 return status.Errorf(codes.Unauthenticated, "no client certificate presented")
119 }
120 if count > 1 {
121 return status.Errorf(codes.Unauthenticated, "exactly one client certificate must be sent (got %d)", count)
122 }
123 pCert := tlsInfo.State.PeerCertificates[0]
124
125 // Ensure that the certificate is signed by the cluster CA.
Serge Bazanskid7d6e022021-09-01 15:03:06 +0200126 if err := pCert.CheckSignatureFrom(l.ClusterCACertificate); err != nil {
Serge Bazanski9ffa1f92021-09-01 15:42:23 +0200127 return status.Errorf(codes.Unauthenticated, "invalid client certificate: %v", err)
128 }
129 // Ensure that the certificate is a client certificate.
130 // TODO(q3k): synchronize this with //metropolis/pkg/pki Client()/Server()/...
131 isClient := false
132 for _, ku := range pCert.ExtKeyUsage {
133 if ku == x509.ExtKeyUsageClientAuth {
134 isClient = true
135 break
136 }
137 }
138 if !isClient {
139 return status.Error(codes.PermissionDenied, "presented certificate is not a client certificate")
140 }
141
142 // MVP: all permissions are granted to all users.
143 // TODO(q3k): check authz.Need once we have a user/identity system implemented.
144 return nil
145}
146
147// streamInterceptor is a gRPC server stream interceptor that performs
148// authentication and authorization of incoming RPCs based on the Authorization
149// option set on each method.
Serge Bazanskid7d6e022021-09-01 15:03:06 +0200150func (l *ServerSecurity) streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
Serge Bazanski9ffa1f92021-09-01 15:42:23 +0200151 if err := l.authorize(ss.Context(), info.FullMethod); err != nil {
152 return err
153 }
154 return handler(srv, ss)
155}
156
157// unaryInterceptor is a gRPC server unary interceptor that performs
158// authentication and authorization of incoming RPCs based on the Authorization
159// option set on each method.
Serge Bazanskid7d6e022021-09-01 15:03:06 +0200160func (l *ServerSecurity) unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
Serge Bazanski9ffa1f92021-09-01 15:42:23 +0200161 if err := l.authorize(ctx, info.FullMethod); err != nil {
162 return nil, err
163 }
164 return handler(ctx, req)
165}