'Configure ASP.NET Core gRPC to use SSL/TLS on both the server and client

I am going to repost this once again. If you don't like my question, there are tons of other posts here that you can apply your personal (and incorrect) judgement to. There is only one issue here: How do you use the ASP.NET Core APIs for gRPC to establish SSL/TSL on BOTH the server and the client... How else can it be stated???

I have searched the ends of 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 put on my big boy pants and use real certificates.



Solution 1:[1]

RANT MODE ON

In my attempts to find a solution, I have found Google searches to be nothing but snipe hunts. Then, in this forum, people find it upon themselves to disparage those who ask questions by asking more stupid questions or spewing unnecessary judgement because a question wasn't asked to their liking. These holier-than-thous don't seem to understand that these people who are doing the asking are asking BECAUSE THEY DON'T KNOW THE ANSWER OR IN MOST CASES, EVEN HOW TO ASK A "PROPER" QUESTION. If you are one of those people, go find something else to do as you are simply wasting your time and the time of people who are also seeking answers.

I have asked several questions over time in this forum. Never once has this forum ever answered one of my questions... I had to solve them on my own so basically, this forum is about 97% useless. But, in the interest of saving someone else time, I am posting a solution.

RANT MODE OFF

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 adn 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
  • Don't be distracted by forum hacks who just want to hear their own voice
  • 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.

Solution 2:[2]

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}");
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2