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
You can disable SSL/TLS by
See http://www.mimekit.net/docs/html/T_MailKit_Security_SecureSocketOptions.htm
Looks like a problem with the certificate or the mail host you are using to connect. To confirm, do this before connecting:
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:
Here’s the custom callback implementation:
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
andchain
parameters may be defined as nullable.