skip to Main Content

When I try to send an email message via Google and the MailKit library on a production containerized application I get this exception:

MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection. The host name did not match the name given in the server’s SSL certificate.

I use ASP.NET Core 5 and Kestrel. Nginx is my reverse proxy. SSL works fine when I get data using Postman. But when I try to send mail, the exception occurs. In a development environment without the Nginx server the proxy works properly.

This is my nginx.conf file:

server {
    client_max_body_size 6M;

    listen 80;

    server_name myhost.com www.myhost.com;

    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    client_max_body_size 6M;

    listen 443 ssl;

    server_name myhost.com www.myhost.com;

    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 0;
    gzip_types text/plain application/javascript text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;

    #SSL code
    ssl_certificate /etc/letsencrypt/live/myhost.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myhost.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    #headers
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-SSL-CERT $ssl_client_escaped_cert;
    
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

   # location /map {
   #    proxy_pass http://client;
   # }

    location /admin {
        proxy_pass http://client-admin;
    }

    location /api {
        proxy_pass http://api:5000;
    }
}

This is my Program.cs file:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MonumentsMap
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureLogging(logging => {
                    logging.ClearProviders();
                    logging.AddConsole();
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    webBuilder.ConfigureKestrel(conf => {
                        conf.Limits.MaxRequestBodySize = 6_000_000;
                    });
                });
    }
}

This is my Startup.cs file:

namespace MonumentsMap
{
    public class Startup
    {

        public Startup(IConfiguration configuration)
        {
            this.Configuration = configuration;

        }
        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(o => o.AddPolicy("WebClientPolicy", builder =>
            {
                builder.AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowAnyOrigin();
            }));
            services.AddControllers().AddNewtonsoftJson(options =>
                options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
            );
            services.AddDbContext<ApplicationContext>(options =>
            {
                options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), o => 
                {
                    o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
                });
            });
            services.AddMemoryCache();

            services.AddRepositories();
            services.AddServices();

            services.AddSingleton(Configuration.GetSection("ImageFilesParams").Get<ImageFilesParams>());
            services.AddScoped<CultureCodeResourceFilter>();
            services.Configure<MailSettings>(Configuration.GetSection("MailSettings"));

            services.AddIdentity<ApplicationUser, IdentityRole>(opts =>
            {
                opts.Password.RequireDigit = true;
                opts.Password.RequireLowercase = true;
                opts.Password.RequireUppercase = true;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequiredLength = 7;
            }).AddEntityFrameworkStores<ApplicationContext>();

            services.AddAuthentication(opts =>
            {
                opts.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = Configuration["Auth:Jwt:Issuer"],
                    ValidAudience = Configuration["Auth:Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(Configuration["Auth:Jwt:Key"])
                    ),
                    ClockSkew = TimeSpan.Zero,
                    RequireExpirationTime = true,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    ValidateAudience = true
                };
            });

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc(name: "v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Monuments Map Api", Version = "v1" });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {

            app.UseExceptionHandler("/errors/500");

            app.UseStatusCodePagesWithReExecute("/errors/{0}");

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.All
            });

            app.UseSwagger();

            app.UseSwaggerUI(c => {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Monuments Map Api V1");
            });

            app.UseCors("WebClientPolicy");

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationContext>();
                var roleManager = serviceScope.ServiceProvider.GetService<RoleManager<IdentityRole>>();
                var userManager = serviceScope.ServiceProvider.GetService<UserManager<ApplicationUser>>();
                context.Database.Migrate();
                var cultures = Configuration.GetSection("SupportedCultures").Get<List<Culture>>();
                DbSeed.Seed(context, roleManager, userManager, cultures, Configuration);
            }
        }
    }
}

MailService.cs file:

namespace MonumentsMap.Data.Services
{
    public class MailService : IMailService
    {
        private readonly MailSettings _mailSettings;

        public MailService(IOptions<MailSettings> mailSettings) => _mailSettings = mailSettings.Value;

        public async Task SendEmailAsync(MailRequestDto mailRequest)
        {
            var email = new MimeMessage();
            email.Sender = MailboxAddress.Parse(_mailSettings.Mail);
            email.To.Add(MailboxAddress.Parse(mailRequest.ToEmail));
            email.Subject = mailRequest.Subject;
            var builder = new BodyBuilder();
            if (mailRequest.Attachments != null)
            {
                byte[] fileBytes;
                foreach (var file in mailRequest.Attachments)
                {
                    if (file.Length > 0)
                    {
                        using (var ms = new MemoryStream())
                        {
                            file.CopyTo(ms);
                            fileBytes = ms.ToArray();
                        }
                        builder.Attachments.Add(file.FileName, fileBytes, ContentType.Parse(file.ContentType));
                    }
                }
            }
            builder.HtmlBody = mailRequest.Body;
            email.Body = builder.ToMessageBody();
            using var smtp = new SmtpClient();
            smtp.Connect(_mailSettings.Host, _mailSettings.Port);
            smtp.Authenticate(_mailSettings.Mail, _mailSettings.Password);
            await smtp.SendAsync(email);
            smtp.Disconnect(true);
        }
    }
}

appsettings.json:

  "MailSettings": {
    "Mail": "[email protected]",
    "DisplayName": "My mail",
    "Password": "application_pass",
    "Host": "smtp.gmail.com",
    "Port": 465
  },

docker-compose file:

version: "3.8"
services:
  api:
    container_name: api
    build: ./Api
    depends_on:
      - db
    restart: unless-stopped
    environment: 
      ASPNETCORE_URLS: http://+:5000
    volumes: 
      - ./Images:/app/Images
  db:
    container_name: db
    image: postgres
    restart: always
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: root
      POSTGRES_DB: api_db
    volumes:
      - ./postgres-data:/var/lib/postgresql/data:rw
  client:
    container_name: client
    build: ./client
    depends_on:
      - api
    restart: unless-stopped
  client-admin:
    container_name: client-admin
    build: ./client-admin
    depends_on:
      - api
    restart: unless-stopped
    stdin_open: true
  nginx:
    image: nginx:stable-alpine
    container_name: docker-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes: 
      - ./nginx/nginx.conf.prod:/etc/nginx/conf.d/nginx.conf
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    depends_on: 
      - client
      - client-admin
      - certbot
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'"
  certbot:
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

2

Answers


  1. You can disable SSL/TLS by

    smtp.Connect(_mailSettings.Host, _mailSettings.Port, **MailKit.Security.SecureSocketOptions.None**);
    

    See http://www.mimekit.net/docs/html/T_MailKit_Security_SecureSocketOptions.htm

    Login or Signup to reply.
  2. Looks like a problem with the certificate or the mail host you are using to connect. To confirm, do this before connecting:

    smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;
    

    This will bypass the certificate validation and should resolve the error. However, if you care about security (and you should), don’t leave it like this. Instead, implement the callback method to figure out why validation is failing. A sample implementation is given here. I’ve reproduced the relevant code below.

    You’ll want to set a callback like we did above:

    smtp.ServerCertificateValidationCallback = MySslCertificateValidationCallback;
    

    Here’s the custom callback implementation:

    static bool MySslCertificateValidationCallback (object sender, X509Certificate certificate, 
           X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
        // If there are no errors, then everything went smoothly.
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;
    
        // Note: MailKit will always pass the host name string as the `sender` argument.
        var host = (string) sender;
    
        if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) {
            // This means that the remote certificate is unavailable. Notify the user and return false.
            Console.WriteLine ("The SSL certificate was not available for {0}", host);
            return false;
        }
    
        if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) {
            // This means that the server's SSL certificate did not match the host name that we are trying to connect to.
            var certificate2 = certificate as X509Certificate2;
            var cn = certificate2 != null ? certificate2.GetNameInfo (X509NameType.SimpleName, false) : certificate.Subject;
    
            Console.WriteLine ("The Common Name for the SSL certificate did not match {0}. Instead, it was {1}.", host, cn);
            return false;
        }
    
        // The only other errors left are chain errors.
        Console.WriteLine ("The SSL certificate for the server could not be validated for the following reasons:");
    
        // The first element's certificate will be the server's SSL certificate (and will match the `certificate` argument)
        // while the last element in the chain will typically either be the Root Certificate Authority's certificate -or- it
        // will be a non-authoritative self-signed certificate that the server admin created. 
        foreach (var element in chain.ChainElements) {
            // Each element in the chain will have its own status list. If the status list is empty, it means that the
            // certificate itself did not contain any errors.
            if (element.ChainElementStatus.Length == 0)
                continue;
    
            Console.WriteLine ("u2022 {0}", element.Certificate.Subject);
            foreach (var error in element.ChainElementStatus) {
                // `error.StatusInformation` contains a human-readable error string while `error.Status` 
                // is the corresponding enum value.
                Console.WriteLine ("tu2022 {0}", error.StatusInformation);
            }
        }
    
        return false;
    }
    

    Put some logging or a breakpoint in this method and step through it to see what certificate is being used, its properties, and where the validation is failing. This should help guide you to the actual source of the problem so you can resolve it.

    Note: Depending on your MailKit version, the certificate and chain parameters may be defined as nullable.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search