diff --git a/cmd/allocator/main.go b/cmd/allocator/main.go index 9fc6097a58..93df955752 100644 --- a/cmd/allocator/main.go +++ b/cmd/allocator/main.go @@ -185,22 +185,42 @@ func (h *serviceHandler) getServerOptions() []grpc.ServerOption { } cfg := &tls.Config{ - Certificates: []tls.Certificate{tlsCer}, - ClientAuth: tls.RequireAndVerifyClientCert, - GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) { - h.certMutex.RLock() - defer h.certMutex.RUnlock() - return &tls.Config{ - Certificates: []tls.Certificate{tlsCer}, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: h.caCertPool, - }, nil - }, + Certificates: []tls.Certificate{tlsCer}, + ClientAuth: tls.RequireAnyClientCert, + VerifyPeerCertificate: h.verifyClientCertificate, } // Add options for creds and OpenCensus stats handler to enable stats and tracing. return []grpc.ServerOption{grpc.Creds(credentials.NewTLS(cfg)), grpc.StatsHandler(&ocgrpc.ServerHandler{})} } +// verifyClientCertificate verifies that the client certificate is accepted +// This method is used as GetConfigForClient is cross lang incompatible. +func (h *serviceHandler) verifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + opts := x509.VerifyOptions{ + Roots: h.caCertPool, + CurrentTime: time.Now(), + Intermediates: x509.NewCertPool(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + for _, cert := range rawCerts[1:] { + opts.Intermediates.AppendCertsFromPEM(cert) + } + + c, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return errors.New("bad client certificate: " + err.Error()) + } + + h.certMutex.RLock() + defer h.certMutex.RUnlock() + _, err = c.Verify(opts) + if err != nil { + return errors.New("failed to verify client certificate: " + err.Error()) + } + return nil +} + // Set up our client which we will use to call the API func getClients() (*kubernetes.Clientset, *versioned.Clientset, error) { // Create the in-cluster config diff --git a/cmd/allocator/main_test.go b/cmd/allocator/main_test.go index 14958771ea..13f7898639 100644 --- a/cmd/allocator/main_test.go +++ b/cmd/allocator/main_test.go @@ -15,6 +15,8 @@ package main import ( "context" + "crypto/x509" + "encoding/pem" "io/ioutil" "net/http" "os" @@ -137,6 +139,38 @@ func TestBadReturnType(t *testing.T) { assert.Contains(t, st.Message(), "internal server error") } +func TestVerifyClientCertificateSucceeds(t *testing.T) { + t.Parallel() + + crt := []byte(clientCert) + certPool := x509.NewCertPool() + assert.True(t, certPool.AppendCertsFromPEM(crt)) + + h := serviceHandler{ + caCertPool: certPool, + } + + block, _ := pem.Decode(crt) + input := [][]byte{block.Bytes} + assert.Nil(t, h.verifyClientCertificate(input, nil), + "verifyClientCertificate failed.") +} + +func TestVerifyClientCertificateFails(t *testing.T) { + t.Parallel() + + crt := []byte(clientCert) + certPool := x509.NewCertPool() + h := serviceHandler{ + caCertPool: certPool, + } + + block, _ := pem.Decode(crt) + input := [][]byte{block.Bytes} + assert.Error(t, h.verifyClientCertificate(input, nil), + "verifyClientCertificate() succeeded, expected error.") +} + func TestGettingCaCert(t *testing.T) { t.Parallel() diff --git a/examples/allocator-client-csharp/Program.cs b/examples/allocator-client-csharp/Program.cs new file mode 100644 index 0000000000..398b44124a --- /dev/null +++ b/examples/allocator-client-csharp/Program.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.IO; +using Grpc.Core; +using V1Alpha1; +using System.Net.Http; + +namespace AllocatorClient +{ + class Program + { + static async Task Main(string[] args) + { + if (args.Length < 6) { + throw new Exception("Arguments are missing. Expecting: "); + } + + string clientKey = File.ReadAllText(args[0]); + string clientCert = File.ReadAllText(args[1]); + string serverCa = File.ReadAllText(args[2]); + string externalIp = args[3]; + string namespaceArg = args[4]; + bool multicluster = bool.Parse(args[5]); + + var creds = new SslCredentials(serverCa, new KeyCertificatePair(clientCert, clientKey)); + var channel = new Channel(externalIp + ":443", creds); + var client = new AllocationService.AllocationServiceClient(channel); + + try { + var response = await client.AllocateAsync(new AllocationRequest { + Namespace = namespaceArg, + MultiClusterSetting = new V1Alpha1.MultiClusterSetting { + Enabled = multicluster, + } + }); + Console.WriteLine(response); + } + catch(RpcException e) + { + Console.WriteLine($"gRPC error: {e}"); + } + } + } +} \ No newline at end of file diff --git a/examples/allocator-client-csharp/README.md b/examples/allocator-client-csharp/README.md new file mode 100644 index 0000000000..af224febe5 --- /dev/null +++ b/examples/allocator-client-csharp/README.md @@ -0,0 +1,19 @@ +# A sample Allocator service C# client + +This sample serves as a gRPC C# client sample code for agones-allocator gRPC service. + +Follow instructions in [Allocator Service](https://agones.dev/site/docs/advanced/allocator-service) to set up client and server certificate. + +Run the following to allocate a game server: +``` +#!/bin/bash + +NAMESPACE=default # replace with any namespace +EXTERNAL_IP=`kubectl get services agones-allocator -n agones-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}'` +KEY_FILE=client.key +CERT_FILE=client.crt +TLS_CA_FILE=ca.crt +MULTICLUSTER_ENABLED=false + +dotnet run $KEY_FILE $CERT_FILE $TLS_CA_FILE $EXTERNAL_IP $NAMESPACE $MULTICLUSTER_ENABLED +``` diff --git a/examples/allocator-client-csharp/allocator-client-csharp.csproj b/examples/allocator-client-csharp/allocator-client-csharp.csproj new file mode 100644 index 0000000000..cd915a633e --- /dev/null +++ b/examples/allocator-client-csharp/allocator-client-csharp.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp3.1 + AllocatorClient + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/allocator-client/main.go b/examples/allocator-client/main.go index 6119196eec..1c1ee9c642 100644 --- a/examples/allocator-client/main.go +++ b/examples/allocator-client/main.go @@ -34,7 +34,7 @@ func main() { cacertFile := flag.String("cacert", "missing cacert", "the CA cert file for server signing certificate in PEM format") externalIP := flag.String("ip", "missing external IP", "the external IP for allocator server") namespace := flag.String("namespace", "default", "the game server kubernetes namespace") - multicluster := flag.Bool("multicluster", false, "enabling") + multicluster := flag.Bool("multicluster", false, "set to true to enable the multi-cluster allocation") flag.Parse() diff --git a/site/content/en/docs/Advanced/allocator-service.md b/site/content/en/docs/Advanced/allocator-service.md index 590d95edd5..e7ce337d19 100644 --- a/site/content/en/docs/Advanced/allocator-service.md +++ b/site/content/en/docs/Advanced/allocator-service.md @@ -115,7 +115,14 @@ kubectl get pods -n agones-system -o=name | grep agones-allocator | xargs kubect ## Send allocation request +{{% feature expiryVersion="1.6.0" %}} Now the service is ready to accept requests from the client with the generated certificates. Create a [fleet](https://agones.dev/site/docs/getting-started/create-fleet/#1-create-a-fleet) and send a gRPC request to agones-allocator by providing the namespace to which the fleet is deployed. You can find the gRPC sample for sending allocation request at {{< ghlink href="examples/allocator-client/main.go" >}}allocator-client sample{{< /ghlink >}}. +{{% /feature %}} + +{{% feature publishVersion="1.6.0" %}} +Now the service is ready to accept requests from the client with the generated certificates. Create a [fleet](https://agones.dev/site/docs/getting-started/create-fleet/#1-create-a-fleet) and send a gRPC request to agones-allocator. To start, take a look at the allocation gRPC client examples in {{< ghlink href="examples/allocator-client/main.go" >}}golang{{< /ghlink >}} and {{< ghlink href="examples/allocator-client-csharp/Program.cs" >}}C#{{< /ghlink >}} languages. In the following, the {{< ghlink href="examples/allocator-client/main.go" >}}golang gRPC client example{{< /ghlink >}} is used to allocate a Game Server in the default namespace. +{{% /feature %}} + ```bash #!/bin/bash