ASP.NET gRPC — OpenSSL Certificate — Digitteck
ASP.NET gRPC — OpenSSL Certificate
dotnet·14 April 2020·5 min read

ASP.NET gRPC — OpenSSL Certificate

Testing a gRPC service from a remote machine fails without a valid certificate — or at least a self-signed one the client can trust. This article walks through generating an OpenSSL certificate with two shell scripts and wiring it into a Kestrel gRPC server, based on excellent write-ups by Daniel Roth and ServiceStack.

1. Install Prerequisites

Install Chocolatey, then install OpenSSL:

javascript
choco install openssl

2. Generate Certificates

Two shell scripts are available — use gen-dev.https.sh for localhost development and gen-prod.https.sh for a real domain. This example uses the prod script (with an explicit password):

javascript
# gen-dev.https.sh — generates a self-signed localhost certificate
PASSWORD=grpc
if [ $# -ge 1 ]; then PASSWORD=$1; fi

cat > dev.config << 'EOT'
[ req ]
default_bits       = 2048
default_md         = sha256
default_keyfile    = dev.key
prompt             = no
encrypt_key        = no
distinguished_name = dn
req_extensions     = v3_req
x509_extensions    = x509_req
string_mask        = utf8only
[ dn ]
commonName             = localhost dev cert
emailAddress           = test@localtest.me
countryName            = US
stateOrProvinceName    = DE
localityName           = Wilmington
organizationName       = Todo World
[ x509_req ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints       = critical, CA:false
keyUsage               = critical, keyEncipherment
subjectAltName         = @alt_names
nsComment              = "OpenSSL Generated Certificate"
[ v3_req ]
subjectKeyIdentifier   = hash
basicConstraints       = critical, CA:false
subjectAltName         = @alt_names
nsComment              = "OpenSSL Generated Certificate"
[ alt_names ]
DNS.1                  = localhost
EOT

openssl req -config dev.config -new -out dev.csr.pem
openssl x509 -req -days 365 -extfile dev.config -extensions v3_req -in dev.csr.pem -signkey dev.key -out dev.crt
openssl pkcs12 -export -out dev.pfx -inkey dev.key -in dev.crt -password pass:$PASSWORD
rm dev.config dev.csr.pem
javascript
# gen-prod.https.sh — generates a certificate for a real domain
DOMAIN=todoworld.servicestack.net
if [ $# -ge 1 ]; then DOMAIN=$1; fi

PASSWORD=grpc
if [ $# -ge 2 ]; then PASSWORD=$2; fi

cat > prod.config << 'EOT'
[ req ]
default_bits       = 2048
default_md         = sha256
default_keyfile    = prod.key
prompt             = no
encrypt_key        = no
distinguished_name = dn
req_extensions     = v3_req
x509_extensions    = x509_req
string_mask        = utf8only
[ dn ]
commonName             = TodoWorld prod cert
emailAddress           = todoworld@servicestack.net
countryName            = US
stateOrProvinceName    = DE
localityName           = Wilmington
organizationName       = Todo World
[ x509_req ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints       = critical, CA:false
keyUsage               = critical, keyEncipherment
subjectAltName         = @alt_names
nsComment              = "OpenSSL Generated Certificate"
[ v3_req ]
subjectKeyIdentifier   = hash
basicConstraints       = critical, CA:false
subjectAltName         = @alt_names
nsComment              = "OpenSSL Generated Certificate"
[ alt_names ]
DNS.1                  = $DOMAIN
EOT

openssl req -config prod.config -new -out prod.csr.pem
openssl x509 -req -days 365 -extfile prod.config -extensions v3_req -in prod.csr.pem -signkey prod.key -out prod.crt
openssl pkcs12 -export -out prod.pfx -inkey prod.key -in prod.crt -password pass:$PASSWORD
rm prod.config prod.csr.pem

Run from PowerShell:

javascript
# dev
./gen-dev.https.sh

# prod
./gen-prod.https.sh your.domain.com grpc
Generated certificate files: prod.crt, prod.key, prod.pfx

3. Kestrel Server — ConfigureEndpoints

The extension method reads endpoint configuration from appsettings.json and loads the certificate from a file path. The certificate password is read from an environment variable mycertpass — never hard-coded. If the environment name is LinuxDevelopment it binds to 0.0.0.0 instead of loopback:

csharp
public static class KestrelServerOptionsExtensions
{
    public static void ConfigureEndpoints(this KestrelServerOptions options)
    {
        var configuration = options.ApplicationServices.GetRequiredService<IConfiguration>();
        var environment   = options.ApplicationServices.GetRequiredService<IWebHostEnvironment>();

        var endpoints = configuration
            .GetSection("HttpServer:Endpoints")
            .GetChildren()
            .ToDictionary(s => s.Key, s =>
            {
                var ep = new EndpointConfiguration();
                s.Bind(ep);
                return ep;
            });

        foreach (var endpoint in endpoints)
        {
            var config = endpoint.Value;
            var port   = config.Port ?? (config.Scheme == "https" ? 443 : 80);

            var ipAddresses = new List<IPAddress>();
            if (config.Host == "localhost")
            {
                if (environment.EnvironmentName == "LinuxDevelopment")
                    ipAddresses.Add(IPAddress.Parse("0.0.0.0"));
                else
                {
                    ipAddresses.Add(IPAddress.IPv6Loopback);
                    ipAddresses.Add(IPAddress.Loopback);
                }
            }
            else if (IPAddress.TryParse(config.Host, out var address))
                ipAddresses.Add(address);
            else
                ipAddresses.Add(IPAddress.IPv6Any);

            foreach (var address in ipAddresses)
            {
                options.Listen(address, port, listenOptions =>
                {
                    if (config.Scheme == "https")
                    {
                        var certPass = Environment.GetEnvironmentVariable("mycertpass")
                            ?? throw new Exception("Environment variable 'mycertpass' not set.");
                        config.Password = certPass;
                        listenOptions.UseHttps(LoadCertificate(config, environment));
                    }
                });
            }
        }
    }

    private static X509Certificate2 LoadCertificate(
        EndpointConfiguration config, IWebHostEnvironment environment)
    {
        if (config.FilePath != null && config.Password != null)
        {
            // Normalise path separators and resolve relative paths
            string[] parts    = config.FilePath.Split('/', '\');
            string fixedPath  = string.Join(Path.DirectorySeparatorChar, parts);
            if (!File.Exists(fixedPath))
                fixedPath = Path.Combine(AppContext.BaseDirectory, fixedPath);
            if (!File.Exists(fixedPath))
                throw new Exception("Certificate not found at the specified path.");
            return new X509Certificate2(fixedPath, config.Password);
        }
        throw new InvalidOperationException("No valid certificate configuration found.");
    }
}

public class EndpointConfiguration
{
    public string Host          { get; set; }
    public int?   Port          { get; set; }
    public string Scheme        { get; set; }
    public string StoreName     { get; set; }
    public string StoreLocation { get; set; }
    public string FilePath      { get; set; }
    public string Password      { get; set; }
}

Replace the Kestrel section in appsettings.json with the custom HttpServer:Endpoints block, then add the .pfx file under a certificates/ folder with "Copy if newer" set in build settings:

javascript
// appsettings.json — replace the Kestrel section with HttpServer:Endpoints
{
  "HttpServer": {
    "Endpoints": {
      "Http": {
        "Host": "localhost",
        "Port": 5000,
        "Scheme": "http"
      },
      "Https": {
        "Host": "localhost",
        "Port": 5001,
        "Scheme": "https",
        "FilePath": "certificates/prod.pfx"
      }
    }
  }
}

Hook it into Program.cs and set the environment variable mycertpass in Windows System Properties (restart Visual Studio after):

csharp
// Program.cs — tell Kestrel to use the custom endpoint configuration
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(options => options.ConfigureEndpoints());
        });

For Docker, pass the environment variable in your docker-compose.yml under environment: - mycertpass=grpc. This is acceptable for development; use secrets management in production.

4. Grpc Client

Add the .crt file to the client project (Copy if newer). Because this is a self-signed certificate with no trusted CA, you must accept it by thumbprint via a custom validation callback:

csharp
// Console gRPC client — trust the self-signed certificate by thumbprint
var certificate = new X509Certificate2("certificates/prod.crt");

var httpHandler = new HttpClientHandler();
httpHandler.ServerCertificateCustomValidationCallback =
    (message, cert, chain, errors) =>
        cert.Thumbprint == certificate.Thumbprint || errors == SslPolicyErrors.None;

var channel = GrpcChannel.ForAddress("https://your-server:5001",
    new GrpcChannelOptions { HttpHandler = httpHandler });

var client = new Greeter.GreeterClient(channel);
var reply  = await client.SayHelloAsync(new HelloRequest { Name = "World" });

Tags

.NETASP.NET CoregRPCSSL
digitteck

© 2026 Digitteck