2

Here is my isue: how do you use the ASP.NET Core APIs for gRPC to establish SSL/TSL on both the server and the client?

I have searched the internet for documentation on how to configure gRPC to use TLS. I can find some people who have posted a "solution" but I personally feel they are hacking it to make it work.

When I look at the Microsoft gRPC documentation along with the relative ASP.NET Core documentation, I know there are many APIs that do what I need to have them do. I just can't find any documentation on how to use them. I know the WHAT, just not the HOW.

I have two types of servers to deploy. One of the "servers" will also be a client in a mesh network topology. One server will be outward-facing with a public IP address. The other servers will be deployed on an individual's personal computer. I am totally lost as to what types of certificates I need to use. I think I got it to work, sort of, using one of the hacks I found. The other servers, I don't know quite what to do.

I used a Microsoft-posted tutorial: "Create a gRPC client and server in ASP.NET Core" and "gRPC services with ASP.NET Core" and was able to get the simple, localhost, Kestrel SSL configuration to work. Now, I need to use real certificates.

halfer
  • 19,824
  • 17
  • 99
  • 186
Steve Miller
  • 300
  • 3
  • 11
  • Looks like you're trying to configure mutual TLS authentication with grpc-dotnet. Server-side certificate is always required to establish TLS connection, so I assume you've figured out that part already (anytime you establish a secure connection from a client, that's what's done basically). Next you need to configure the client to also authenticate itself to the server via its certificate. Doing that is documented here: https://learn.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz?view=aspnetcore-5.0#client-certificate-authentication – Jan Tattermusch Oct 08 '21 at 12:28

2 Answers2

3

There's a Mutual-SSL/TLS-Example how to use a gRPC-AspNetCore-Server with a native Grpc.Core.Client and a managed Grpc.Net.Client in my OpenSource Project.

Just navigate to .\SiLA2.gRPC.CSharp\Examples\TemperatureController.

AspNetCore.Server:

public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>(); 
                webBuilder.ConfigureKestrel(opt =>
                {
                    //### Option ListenLocalhost can be used for native Grpc.Core-Clients by http ->  SslCredentials.Insecure #######
                    //###############################################################################################################
                    opt.ListenLocalhost(13745, o => o.Protocols = HttpProtocols.Http2);//############################################
                    //###############################################################################################################

                    var config = (IConfiguration)opt.ApplicationServices.GetService(typeof(IConfiguration));
                    var cert = new X509Certificate2(config["Certificate:File"],
                                                    config["Certificate:Password"]);

                    opt.ConfigureHttpsDefaults(h =>
                    {

                        h.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
                        h.CheckCertificateRevocation = false;
                        h.ServerCertificate = cert;
                    });
                });
            });

Startup.cs :

public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc(x => x.EnableDetailedErrors = true);
        services.AddAuthentication().AddCertificate(opt =>
        {
            opt.AllowedCertificateTypes = CertificateTypes.SelfSigned;
            opt.RevocationMode = X509RevocationMode.NoCheck; // Self-Signed Certs (Development)
            opt.Events = new CertificateAuthenticationEvents()
            {
                OnCertificateValidated = ctx =>
                {
                    // Write additional Validation  
                    ctx.Success();
                    return Task.CompletedTask;
                }
            };
        });

Grpc.Net.Client :

var cert = new X509Certificate2(_configuration["Service:CertFileName"]);
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback =
                    HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
        handler.ClientCertificates.Add(cert);
        var client = new HttpClient(handler);
        var opt = new GrpcChannelOptions()
        {
            HttpClient = client,
            LoggerFactory = serviceProvider.GetService<ILoggerFactory>()
        };

        var channel = GrpcChannel.ForAddress(_configuration["Connection:gRPCServerHostURI"], opt);
        var siLA2ServiceClient = new SiLAService.SiLAServiceClient(channel);

Grpc.Core.Client :

class Program
{
    const string HOST = "localhost";

    static void Main(string[] args)
    {
        const int SECURE_PORT = 13746;
        const int INSECURE_PORT = 13745;

        try
        {
            Console.WriteLine($"{Environment.NewLine}Sending Server-Name-Request SiLA2 Server by Native gRPC-Client...");
            GetServerResponse(SECURE_PORT, new SslCredentials(File.ReadAllText($"SiLA2DevExampleSelfCert2.pem")), "Sending secure (https) Server-Name-Request SiLA2 Server by Managed gRPC-Client ");
            GetServerResponse(INSECURE_PORT, ChannelCredentials.Insecure, "Sending unencrypted (http) Server-Name-Request SiLA2 Server by Native gRPC-Client by http ");
        }
        catch (RpcException e)
        {
            Console.WriteLine(e);
        }
        Console.ReadKey();
    }

    private static void GetServerResponse(int port, ChannelCredentials sslCredentials, string requestText)
    {
        var channel = new Channel($"{HOST}:{port}", sslCredentials);
        Console.WriteLine($"{Environment.NewLine}{requestText} ({channel.Target})");
        var silaService = new SiLAServiceClient(channel);
        var response = silaService.Get_ServerName(new Get_ServerName_Parameters());
        Console.WriteLine($"{Environment.NewLine}Response from SiLA2 Server : {response.ServerName}");
    }
}
Chamundi
  • 41
  • 6
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 04 '22 at 13:15
  • Thanks for this awesome addition! I found it to be VERY helpful!!\ – Steve Miller Jan 17 '22 at 14:57
  • More gratitude :) – David Gerding Nov 29 '22 at 22:20
2

My issue was how to configure a gRPC server to use TLS. Using OpenSSL, there are a myriad of ways to create certificate files, but you have to know exactly which set of commands to use to get the type of certificate you need. In my case, I am using Ubuntu 20.04 Server, Visual Studio 2022, C#, ASP.NET Core 6, and gRPC.

The base issue was how to configure Kestrel to use a certificate and listen to a set of specific ports. The Microsoft documentation is pitiful in that it will give you the signature of an API call, but almost NEVER will show even a minute code snippet. I could find no examples in this or any other Google search. I basically brute forced my way to success.

I have two profiles in launchSettings.json:

{
    "profiles": {
        "OperationsServicesDev": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": false,
            "applicationUrl": "https://localhost:30051",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development",
                "ASPNETCORE_PREVENTHOSTINGSTARTUP": "true"
            }
        },
        "OperationsServicesProd": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": false,
            "applicationUrl": "https://some.node.com:30051",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Production",
                "ASPNETCORE_PREVENTHOSTINGSTARTUP": "true"
            }
        }
    }
}

I use the various profiles to control things that are different in the Linux versus Windows worlds such as log folders, ports, etc. I also make use of appsettigns.Development.json and appsettings.Production.json to further control the differences in environments. The development profile always worked as I was testing in a development environment using a dev cert. When I deployed the published works to my server, the wheels stopped turning. I would get a denial of service, or a timeout, or a vague error about the certificate not passing the parsing stage.

I have a valid set of certs from Godaddy.com. I will come back to this in a bit.

Since projects in .NET 6 have had their templates changed, this added more time brute-forcing stuff, but I figured that out. Startup.cs is no longer used (I guess you can still create one and use it, but all of the configuration is now done in Program.cs).

I start off getting access to the various builders:

// Start the app configuration.  Enlist the help of the various
// .Net Core builders.
var builder = WebApplication.CreateBuilder (args);
var webHostBuilder = builder.WebHost;

// What environment are we running?  It could be either Development
// or Production.  This is used to get the correct configuration from
// the appsettings.Development.json or appsettings.Production.json
// files.
var environment = builder.Environment.EnvironmentName;

Then, using NLog, I configure the loggers:

// Configure logging.
_ = webHostBuilder.ConfigureLogging (options => {

    // Get the correct appsettigns.environment.json file.
    var config = new ConfigurationBuilder ()
        .SetBasePath (Directory.GetCurrentDirectory ())
        .AddJsonFile ($"appsettings.{environment}.json", optional: true, reloadOnChange: true).Build ();

    _ = options.AddNLog (config);

    // Get the logger.
    LogManager.Configuration = new NLogLoggingConfiguration (config.GetSection ("NLog"));

    logger = NLogBuilder.ConfigureNLog (LogManager.Configuration).GetCurrentClassLogger () as NLog.ILogger;
});

Next it's time to configure Kestrel:

// Configure Kestrel, the .NET Core web server.
var hostBuilder = webHostBuilder.ConfigureKestrel (kestrelServerOptions => {

    kestrelServerOptions.ConfigureHttpsDefaults (httpsConnectionAdapterOptions => httpsConnectionAdapterOptions.SslProtocols = SslProtocols.Tls12);

    // Read in the X.509 certificate file.
    var certPath = Path.Combine (builder.Environment.ContentRootPath, "Certs", $"xxx-{environment}.pfx");

    kestrelServerOptions.ConfigureEndpointDefaults (listenOptions => {

        _ = listenOptions.UseHttps (certPath, password);

        logger.Debug ($"Using {certPath} as the cert file.");
        logger.Debug ("Configuring host to use HTTP/2 protocol.");

        listenOptions.Protocols = HttpProtocols.Http2;
    });

    logger.Debug ("Reading config values for the server name and port.");

    // Get the host name and port number to bind the service to.
    var port = builder.Configuration.GetValue<int> ("AppSettings:OperationsServerPort");
    var address = IPAddress.Parse ("0.0.0.0");

    if (address != null) {
        logger.Debug ($"Host will listen at https://{address}:{port}");

        kestrelServerOptions.Listen (address, port);
    } else {
        logger.Error ("DNS address for service host cannot be determined!  Exiting...");

        Environment.Exit (-1);
    }
});

When errors occur on the server side, it is helpful for the gRPC pipeline to give some clues on exceptions thrown:

// Configure gRPC exception handling.
_ = builder.Services.AddGrpc (grpcServiceOptions => {

    grpcServiceOptions.Interceptors.Add<ServerLoggerInterceptor> ();

    _ = grpcServiceOptions.EnableDetailedErrors = true;
});

The rest is boilerplate.

Now, the crucial thing is in creating the .pfx cert file. I had originally been including the Godaddy CA bundle file, typically named gd_bundle-g2-g1.crt. When I added several certs to the Ubuntu trust store (and this Godaddy bundle was one of the files I loaded into the trust store) I noticed an error when I did a

sudo update-ca-certificates  --fresh

I got this extremely helpful clue:

Clearing symlinks in /etc/ssl/certs... done. Updating certificates in /etc/ssl/certs... rehash: warning: skipping gd_bundle-g2-g1.pem,it does not contain exactly one certificate or CRL 131 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done.

This was the OpenSSL command I used to create the bad certificate file:

openssl pkcs12 -export -out certificate.pfx -inkey server.key -in 1baa5781b0db93d3.crt -certfile gd_bundle-g2-g1.crt

The last bit, the -certfile, was the issue. When I regenerated the certificate using the following command:

openssl pkcs12 -export -out certificate.pfx -inkey server.key -in 1baa5781b0db93d3.crt

everything started to work like a charm.

Key takeaways:

  • Be tenacious
  • Don't listen to the naysayers
  • Read the documentation and then read it again and then read it again
  • Download the source and read that, too.

Somewhere along the way, you will find a way to be successful.

halfer
  • 19,824
  • 17
  • 99
  • 186
Steve Miller
  • 300
  • 3
  • 11
  • @JamesNK (James Newton-King) would it be possible to include WAY more in terms of code snippets and crucial things like certificate files and how to make them useful for gRPC? Your samples take you to the intersections, but never tell you if you need to make a left, make a right, or plow straight ahead... – Steve Miller Oct 11 '21 at 21:53
  • @JanTattermusch, you were correct in your observations. I was describing a mutual TLS authentication scenario. I found I didn't need to take it that far and was able to get it all to work via my diatribe above. Thanks for your comment, thought! That, I DID find helpful!! ;-) – Steve Miller Oct 11 '21 at 21:56
  • if you were kind enough to offer any references or adjustments to your example when I've already got the cert in azure with the rest of it: web service+kestrel+docker. And there's an open issue in the Grpc-dotnet repo. https://github.com/grpc/grpc-dotnet/issues/1474. Thanks either way. – David Gerding Nov 10 '21 at 01:11