Open ststeiger opened 3 years ago
It's not natively supported, but you can plug it in. Here's an example: https://github.com/aspnet/AspLabs/blob/452634b6aff935dba07181fe70f614d5788076e0/src/ProxyProtocol/ProxyProtocol.Sample/Program.cs#L38
Notes for triage: This makes the third request for this feature (the other two were internal). It's not a lot, but it's slowly gaining interest.
Golang doesn't have this built in or maybe you meant something else because that link above is a library written in go, not the core libraries...
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
@Tratcher: Thanks for that ! Had a fight with HAproxy config file. And the result is - WoW, cool, it looks like it works, even with wildcard SNI ! I will check that in detail this evening.
@davidfowl: What I meant by that was that there's a library out there to support proxy-protocol since 4 years. I searched for a .NET/Core library on google and found nothing. But the code from AspLabs seems to work for what I have tested so far - so this is solved now, for the time being anyway. Don't know if this works for gzip/br content, but I will find out. Now whatever the finding is, I have something to build on.
@Tratcher: There was another public one: https://github.com/dotnet/aspnetcore/issues/10645 unless strainovic works for msft. Since that issue had no solution, I opened this issue.
@ststeiger Thanks, I for got about that one.
@Tratcher 👋 I'll add one more to the feature request count if I can. I'll try out the example you posted but it would be awesome if it was built in. In AWS using the Network Load Balancers, if you want to see the client's IP and you are registering targets using IP address (as is the case when using Fargate) they suggest enabling Proxy Protocol v2 as well.
It doesn't need to be built in. I'd like to see the mileage we get with that sample code before we prioritize it. I don't this this is built into many platform webservers out there.
It'll be nice to have this officially supported in .NET Core. We're trying to use Azure Private Link Service with TCP Proxy v2 in some backend services for Azure Cosmos DB and that requires supporting proxy-protocol. For now I'll give this sample a try and see if it works.
It works with nginx. It doesn't work with SSL passthrough in HAproxy, for unknown reason. Maybe I don't need it in HAproxy when I do SSL passthrough, though.
@Tratcher: One more thing: That doesn't work with SSL passthrough in HAproxy. It works with nginx. I guess it's a bug or missing-feature somewhere.
Here my HAproxy config, for reproduction:
# /etc/haproxy/haproxy.cfg
# Validate:
# haproxy -c -V -f /etc/haproxy/haproxy.cfg
# Another way is to
# sudo service haproxy configtest
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http
bind *:80
mode http
option forwardfor
# option httpchk /check.cfm
# use-server server1 if { hdr(host) -i server1.domain.net }
# use-server server2 if { hdr(host) -i server2.domain.net }
# server server1 localhost:22201 check
# server server2 localhost:22202 check
# default_backend nodes
# redirect scheme https code 301 if !{ ssl_fc }
# http://192.168.1.2/.well-known/acme-challenge/token.txt
# http://88.84.21.77/.well-known/acme-challenge/token.txt
# http://daniel-steiger.ch/.well-known/acme-challenge/token.txt
# http://henri-bernhard.ch/.well-known/acme-challenge/token.txt
# https://www.haproxy.com/documentation/aloha/12-5/traffic-management/lb-layer7/acls/
# For ACLs sharing the same name, the following rules apply:
# It is possible to use the same <aclname> for many ACLs, even if they do not have the same matching criterion
# A logical OR applies between all of them
# acl daniel_steiger_ch dst 192.168.1.2
# acl daniel_steiger_ch dst 88.84.21.77
acl daniel_steiger_ch hdr(host) -i 88.84.21.77
acl daniel_steiger_ch hdr(host) -i 192.168.1.2
acl daniel_steiger_ch hdr(host) -i daniel-steiger.ch
acl daniel_steiger_ch hdr(host) -m end .daniel-steiger.ch
acl henri_bernhard_ch hdr(host) -i henri-bernhard.ch
acl henri_bernhard_ch hdr(host) -m end .henri-bernhard.ch
#use_backend http_daniel_steiger_ch if { hdr(host) -i daniel-steiger.ch }
#use_backend http_daniel_steiger_ch if { hdr(host) -m end .daniel-steiger.ch }
use_backend http_daniel_steiger_ch if daniel_steiger_ch
use_backend http_henri_bernhard_ch if henri_bernhard_ch
backend http_daniel_steiger_ch
mode http
balance roundrobin
server web0 127.0.0.1:5006
backend http_henri_bernhard_ch
mode http
balance roundrobin
server web0 127.0.0.1:5008
#backend nodes
# mode http
# balance roundrobin
# option forwardfor
# reqirep ^Host: Host:\ node1.myapp.mycompany.com
# server web01 node1.myapp.mycompany.com:80
# sudo systemctl stop nginx
# sudo systemctl disable nginx
# sudo systemctl enable haproxy
# service haproxy start
# sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
# service haproxy restart
frontend https
bind *:443
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
#tcp-request content accept if { req_ssl_hello_type 1 }
# https://datamakes.com/2018/02/17/high-intensity-port-sharing-with-haproxy/
# systemctl restart sshd
# systemctl disable sshd
# systemctl enable sshd
# sudo apt-get install openssh-server
# sudo systemctl status ssh
# sudo ufw allow ssh
# sudo ufw enable
# sudo ufw status
# ufw allow 443/tcp
# ufw allow 8443/tcp
# /mnt/sshfs/var/www/.dotnet/corefx/cryptography/crls/
# sudo apt-get install exfat-utils exfat-fuse
# https://192.168.1.2/.well-known/acme-challenge/token.txt
# https://88.84.21.77/.well-known/acme-challenge/token.txt
# http://daniel-steiger.ch/.well-known/acme-challenge/token.txt
# http://henri-bernhard.ch/.well-known/acme-challenge/token.txt
# https://www.haproxy.com/documentation/aloha/12-5/traffic-management/lb-layer7/acls/
# For ACLs sharing the same name, the following rules apply:
# It is possible to use the same <aclname> for many ACLs, even if they do not have the same matching criterion
# A logical OR applies between all of them
# sequence matters !
# having these two lines here blocks ssh if use_backend openssh comes afterwards ...
# also, this fucks up SNI ...
# acl daniel_steiger_ch dst 192.168.1.2
# acl daniel_steiger_ch dst 88.84.21.77
acl daniel_steiger_ch req_ssl_sni -i daniel-steiger.ch
acl daniel_steiger_ch req.ssl_sni -m end .daniel-steiger.ch
acl henri_bernhard_ch req_ssl_sni -i henri-bernhard.ch
acl henri_bernhard_ch req.ssl_sni -m end .henri-bernhard.ch
# wildcard
use_backend https_daniel_steiger_ch if daniel_steiger_ch
use_backend https_henri_bernhard_ch if henri_bernhard_ch
# use_backend example_int if { req_ssl_sni -i example.int }
# use_backend example_int if { req_ssl_sni -m end .example.int }
# use_backend example_int if { req_ssl_sni -i example.int }
# use_backend foo_int if { req_ssl_sni -i foo.int }
# use_backend bar_int if { req_ssl_sni -i bar.int }
# sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
backend https_daniel_steiger_ch
mode tcp
balance roundrobin
server web0 127.0.0.1:5005 # send-proxy-v2
backend https_henri_bernhard_ch
mode tcp
balance roundrobin
server web0 127.0.0.1:5007 # send-proxy-v2
backend foo_int
balance roundrobin
server web1 127.0.0.1:5005 send-proxy
backend bar_int
balance roundrobin
server web2 127.0.0.1:5005 ##send-proxy
@ststeiger You'll need to figure out which to process first, the TLS or the PPv2. The order you add them is the order they're applied.
TLS first:
options.ListenAnyIP(5000, listenOptions =>
{
listenOptions.UseHttps(...);
listenOptions.Use(async (connectionContext, next) =>
{
await ProxyProtocol.ProcessAsync(connectionContext, next, logger);
});
});
PPv2 first:
options.ListenAnyIP(5000, listenOptions =>
{
listenOptions.Use(async (connectionContext, next) =>
{
await ProxyProtocol.ProcessAsync(connectionContext, next, logger);
});
listenOptions.UseHttps(...);
});
@ststeiger @Tratcher very illuminating! This is exciting. What is the recommended setup to make it work with HTTP2? Any references welcome.
@georgiosd: I think this would be TCP/UDP level. On my test-website, it already works with HTTP 2.0 and gzip/brotli/deflate. See https://henri-bernhard.ch. Chrome shows protocol as h2.
@Tratcher: Thanks in hindsight for that tip. It was exactly that - nginx first decrypted, then proxied, then re-encryptedn, then forwarded. HAproxy instead takes the encrypted - then proxies - then forwards - in passthrough-proxy mode.
If you think about it, it's logical. Finding that one without knowing what you're looking for would have been hard. So thanks again - much obliged !
@georgiosd: Well, I can give you what I have, but it's not necessarely a recommended setup. I think the major pitfalls are LetsEncrypt, HSTS with HTTPS-Redirection (the port), and SSL-certificate synchronization with the web-server, if you don't use SSL-passthrough (seriously, use SSL-passthrough, otherwise it's f***ed).
I think the important part is
Here My Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using AspNetCore.SEOHelper;
namespace HomepageHenri
{
public class Startup
{
protected IConfiguration m_configuration;
protected Microsoft.AspNetCore.Hosting.IWebHostEnvironment m_environment;
public Startup(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IWebHostEnvironment env)
{
this.m_configuration = configuration;
this.m_environment = env;
HomepageHenri.WebTools.Configure(env);
//var builder = new ConfigurationBuilder()
// .SetBasePath(env.ContentRootPath)
// .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
// .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
// .AddEnvironmentVariables();
//Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigureOptions<KestrelServerOptions>, KestrelOptionsSetup>(); // Why transient ?
services.AddHttpsRedirection(
delegate(HttpsRedirectionOptions options)
{
options.RedirectStatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status308PermanentRedirect;
bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
bool behindReverseProxy = "INSERT_PRODUCTION_SERVER_HOST_NAME_HERE".Equals(System.Environment.MachineName, System.StringComparison.InvariantCultureIgnoreCase);
bool useKestrel = true;
// https://stackoverflow.com/questions/42272021/check-if-asp-netcore-application-is-hosted-in-iis
// https://docs.microsoft.com/en-us/previous-versions/iis/6.0-sdk/ms524602%28v%3Dvs.90%29
if (System.Environment.GetEnvironmentVariable("APP_POOL_ID") is string)
useKestrel = false;
if (useKestrel || !isWindows)
{
PseudoUrl url = this.m_configuration.GetValue<string>("Kestrel:EndPoints:Https:Url", null);
if (url != null)
options.HttpsPort = url.Port;
else
options.HttpsPort = 17005;
// if(!isWindows || !this.m_environment.IsDevelopment())
// { options.HttpsPort = 443; }
}
else
{
// PseudoUrl url = this.Configuration.GetValue<string>("iisSettings:iisExpress:applicationUrl", null);
options.HttpsPort = this.m_configuration.GetValue<int>("iisSettings:iisExpress:sslPort", 443);
}
if (useKestrel && behindReverseProxy)
options.HttpsPort = 443;
}
);
//services.AddHsts(options =>
//{
// options.Preload = true;
// options.IncludeSubDomains = true;
// options.MaxAge = TimeSpan.FromDays(60);
// options.ExcludedHosts.Add("example.com");
// options.ExcludedHosts.Add("www.example.com");
//});
services.AddControllersWithViews();
// https://dotnetcoretutorials.com/2017/01/24/using-gzip-compression-asp-net-core/
// services.AddResponseCompression();
services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add(new BrotliCompressionProvider());
options.Providers.Add(new GzipCompressionProvider());
options.Providers.Add(new DeflateCompressionProvider());
options.MimeTypes = new[] {
"text/plain", "text/html", "text/css"
,"application/javascript", "application/json", "application/xml"
,"image/x-icon", "image/png", "image/gif", "image/jpeg", "image/webp", "image/tiff", "image/svg+xml"
};
});
services.AddSingleton<Microsoft.AspNetCore.Http.IHttpContextAccessor, Microsoft.AspNetCore.Http.HttpContextAccessor>();
services.AddSingleton<Microsoft.AspNetCore.Mvc.Infrastructure.IActionContextAccessor, Microsoft.AspNetCore.Mvc.Infrastructure.ActionContextAccessor>();
// For use in _layout.cs
services.AddTransient<HomepageHenri.Model.Navigation.MainNavigationData>();
// Quartz.IJob
services.AddTransient<CertificateRefreshJob>();
services.AddSingleton<JobScheduler>();
}
public void Configure(IApplicationBuilder app
, IWebHostEnvironment env
, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory)
{
string virtual_directory = "/Virt_DIR";
virtual_directory = "/";
if (virtual_directory.EndsWith("/"))
virtual_directory = virtual_directory.Substring(0, virtual_directory.Length - 1);
if (string.IsNullOrWhiteSpace(virtual_directory))
ConfigureMapped(app, env, loggerFactory); // Don't map if you don't have to
else
// app.Map("/Virt_DIR", (mappedApp) => Configure1(mappedApp, env, loggerFactory));
app.Map(virtual_directory, delegate (IApplicationBuilder mappedApp)
{
ConfigureMapped(mappedApp, env, loggerFactory);
}
);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void ConfigureMapped(IApplicationBuilder app, IWebHostEnvironment env
, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory
)
{
//loggerFactory.AddConsole(Configuration.GetSection("Logging"));
//loggerFactory.AddDebug();
System.Web.HttpContext.Configure(app.ApplicationServices.
GetRequiredService<Microsoft.AspNetCore.Http.IHttpContextAccessor>()
);
System.Web.HttpContext.ConfigureActionContext(app.ApplicationServices.
GetRequiredService<Microsoft.AspNetCore.Mvc.Infrastructure.IActionContextAccessor>()
);
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor
| Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
| Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedHost
});
app.UseMiddleware<LetsEncryptChallengeApprovalMiddleware>();
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// app.UseDeveloperExceptionPage(new DeveloperExceptionPageOptions() { SourceCodeLineCount = 100 });
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
// app.UseStatusCodePages();
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-6.0#:~:text=The%20Developer%20Exception%20Page%20displays,running%20in%20the%20Development%20environment.&text=Detailed%20exception%20information%20shouldn't,runs%20in%20the%20Production%20environment.
app.UseStatusCodePages(
async delegate(Microsoft.AspNetCore.Diagnostics.StatusCodeContext statusCodeContext )
{
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = System.Net.Mime.MediaTypeNames.Text.Html;
string errorDesc = HttpErrorHelper.GetStatusDescription(statusCodeContext.HttpContext.Response.StatusCode);
await statusCodeContext.HttpContext.Response.WriteAsync(
$@"<!DOCTYPE html>
<html>
<head>
<title>Error: {errorDesc}
</title>
</head>
<body>
<h1>HTTP-Status-Code: {statusCodeContext.HttpContext.Response.StatusCode}</h1>
<a href=""https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#{statusCodeContext.HttpContext.Response.StatusCode}""><p>{errorDesc}</p></a>
<p>Please move on</p>
</body>
</html>");
}
);
// app.UseMiddleware<CustomMiddleware>();
// // app.UseMiddleware<ErrorMiddleware>();
// app.UseHttpsRedirection();
// https://stackoverflow.com/questions/52347936/exclude-route-from-middleware-net-core
app.UseWhen(
delegate (Microsoft.AspNetCore.Http.HttpContext httpContext)
{
// https://localhost:44396/.well-known/acme-challenge/token.txt
// https://localhost:44396/Profile
// http://localhost:17006/.well-known/acme-challenge/token.txt
// https://localhost:17005/.well-known/acme-challenge/token.txt
// https://localhost:17005/Profile
return !httpContext.Request.Path.StartsWithSegments("/.well-known/acme-challenge");
}
,
delegate (IApplicationBuilder appBuilder)
{
// appBuilder.UseHttpsRedirection();
appBuilder.UseDebugHttpsRedirection();
appBuilder.UseMiddleware<DebugHttpsRedirectionMiddleware>();
}
);
// Permanently redirect www to top-level-domain
RewriteOptions options = new RewriteOptions();
options.AddRedirectToNonWwwPermanent();
options.AddRedirectToHttps();
app.UseRewriter(options);
app.UseRobotsTxt(env.WebRootPath);
SitemapHelper.CreateSitemap(env.WebRootPath);
app.UseXMLSitemap(env.WebRootPath);
app.UseRouting();
// https://stackoverflow.com/questions/60791843/changing-routedata-in-asp-net-core-3-1-in-middleware
// Note: Sequence matters ! After app.UseRouting, but before app.UseEndpoints
app.UseHostHeaderData();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
app.UseCaptchaMiddleWare();
app.UseDefaultFiles(new DefaultFilesOptions()
{
DefaultFileNames = new System.Collections.Generic.List<string>()
{
"index.htm", "index.html"
}
});
// https://stackoverflow.com/questions/38231739/how-to-disable-browser-cache-in-asp-net-core-rc2
// https://stackoverflow.com/questions/33342643/how-does-javascript-version-asp-append-version-work-in-asp-net-core-mvc
app.UseStaticFiles(new StaticFileOptions()
{
ServeUnknownFileTypes = true,
DefaultContentType = "application/octet-stream",
ContentTypeProvider = new ExtensionContentTypeProvider(),
OnPrepareResponse = delegate (Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext context)
{
// https://stackoverflow.com/questions/49547/how-do-we-control-web-page-caching-across-all-browsers
// The Cache-Control is per the HTTP 1.1 spec for clients and proxies
// If you don't care about IE6, then you could omit Cache-Control: no-cache.
// (some browsers observe no-store and some observe must-revalidate)
context.Context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0";
// Other Cache-Control parameters such as max-age are irrelevant
// if the abovementioned Cache-Control parameters (no-cache,no-store,must-revalidate) are specified.
// Expires is per the HTTP 1.0 and 1.1 specs for clients and proxies.
// In HTTP 1.1, the Cache-Control takes precedence over Expires, so it's after all for HTTP 1.0 proxies only.
// If you don't care about HTTP 1.0 proxies, then you could omit Expires.
context.Context.Response.Headers["Expires"] = "-1, 0, Tue, 01 Jan 1980 1:00:00 GMT";
// The Pragma is per the HTTP 1.0 spec for prehistoric clients, such as Java WebClient
// If you don't care about IE6 nor HTTP 1.0 clients
// (HTTP 1.1 was introduced 1997), then you could omit Pragma.
context.Context.Response.Headers["pragma"] = "no-cache";
// On the other hand, if the server auto-includes a valid Date header,
// then you could theoretically omit Cache-Control too and rely on Expires only.
// Date: Wed, 24 Aug 2016 18:32:02 GMT
// Expires: 0
// But that may fail if e.g. the end-user manipulates the operating system date
// and the client software is relying on it.
// https://stackoverflow.com/questions/21120882/the-date-time-format-used-in-http-headers
}
});
JobScheduler scheduler = app.ApplicationServices.GetRequiredService<JobScheduler>();
_ = scheduler.StartJobs();
}// End Sub
} // End Class Startup
} // End Namespace HomepageHenri
And this is the haproxy config file
# /etc/haproxy/haproxy.cfg
# Validate:
# haproxy -c -V -f /etc/haproxy/haproxy.cfg
# Another way is to
# sudo service haproxy configtest
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http
bind *:80
mode http
option forwardfor
# option httpchk /check.cfm
# use-server server1 if { hdr(host) -i server1.domain.net }
# use-server server2 if { hdr(host) -i server2.domain.net }
# server server1 localhost:22201 check
# server server2 localhost:22202 check
# default_backend nodes
# redirect scheme https code 301 if !{ ssl_fc }
# http://10.0.1.2/.well-known/acme-challenge/token.txt
# http://88.84.21.77/.well-known/acme-challenge/token.txt
# http://daniel-steiger.ch/.well-known/acme-challenge/token.txt
# http://henri-bernhard.ch/.well-known/acme-challenge/token.txt
# https://www.haproxy.com/documentation/aloha/12-5/traffic-management/lb-layer7/acls/
# For ACLs sharing the same name, the following rules apply:
# It is possible to use the same <aclname> for many ACLs, even if they do not have the same matching criterion
# A logical OR applies between all of them
# acl daniel_steiger_ch dst 10.0.1.2
# acl daniel_steiger_ch dst 88.84.21.77
acl daniel_steiger_ch hdr(host) -i 88.84.21.77
acl daniel_steiger_ch hdr(host) -i 10.0.1.2
acl daniel_steiger_ch hdr(host) -i daniel-steiger.ch
acl daniel_steiger_ch hdr(host) -m end .daniel-steiger.ch
acl henri_bernhard_ch hdr(host) -i henri-bernhard.ch
acl henri_bernhard_ch hdr(host) -m end .henri-bernhard.ch
#use_backend http_daniel_steiger_ch if { hdr(host) -i daniel-steiger.ch }
#use_backend http_daniel_steiger_ch if { hdr(host) -m end .daniel-steiger.ch }
use_backend http_daniel_steiger_ch if daniel_steiger_ch
use_backend http_henri_bernhard_ch if henri_bernhard_ch
backend http_daniel_steiger_ch
mode http
balance roundrobin
server web0 127.0.0.1:17006
backend http_henri_bernhard_ch
mode http
balance roundrobin
server web0 127.0.0.1:17008
#backend nodes
# mode http
# balance roundrobin
# option forwardfor
# reqirep ^Host: Host:\ node1.myapp.mycompany.com
# server web01 node1.myapp.mycompany.com:80
# sudo systemctl stop nginx
# sudo systemctl disable nginx
# sudo systemctl enable haproxy
# service haproxy start
# sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
# service haproxy restart
frontend https
bind *:443
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
#tcp-request content accept if { req_ssl_hello_type 1 }
# https://datamakes.com/2018/02/17/high-intensity-port-sharing-with-haproxy/
# systemctl restart sshd
# systemctl disable sshd
# systemctl enable sshd
# sudo apt-get install openssh-server
# sudo systemctl status ssh
# sudo ufw allow ssh
# sudo ufw enable
# sudo ufw status
# ufw allow 443/tcp
# ufw allow 8443/tcp
# /etc/ssh/sshd_config ==> PermitRootLogin yes + PasswordAuthentication no + ChallengeResponseAuthentication no ~/.ssh/id_rsa.pub ==> ~/.ssh/authorized_keys
acl ssh_payload payload(0,7) -m bin 5353482d322e30
# /mnt/sshfs/var/www/.dotnet/corefx/cryptography/crls/
# sudo apt-get install exfat-utils exfat-fuse
# https://10.0.1.2/.well-known/acme-challenge/token.txt
# https://88.84.21.77/.well-known/acme-challenge/token.txt
# http://daniel-steiger.ch/.well-known/acme-challenge/token.txt
# http://henri-bernhard.ch/.well-known/acme-challenge/token.txt
# https://www.haproxy.com/documentation/aloha/12-5/traffic-management/lb-layer7/acls/
# For ACLs sharing the same name, the following rules apply:
# It is possible to use the same <aclname> for many ACLs, even if they do not have the same matching criterion
# A logical OR applies between all of them
# sequence matters !
use_backend openssh if ssh_payload
use_backend openssh if !{ req.ssl_hello_type 1 } { req.len 0 }
# having these two lines here blocks ssh if use_backend openssh comes afterwards ...
# also, this fucks up SNI ...
# acl daniel_steiger_ch dst 10.0.1.2
# acl daniel_steiger_ch dst 88.84.21.77
acl daniel_steiger_ch req_ssl_sni -i daniel-steiger.ch
acl daniel_steiger_ch req.ssl_sni -m end .daniel-steiger.ch
acl henri_bernhard_ch req_ssl_sni -i henri-bernhard.ch
acl henri_bernhard_ch req.ssl_sni -m end .henri-bernhard.ch
# wildcard
use_backend https_daniel_steiger_ch if daniel_steiger_ch
use_backend https_henri_bernhard_ch if henri_bernhard_ch
# use_backend example_int if { req_ssl_sni -i example.int }
# use_backend example_int if { req_ssl_sni -m end .example.int }
# use_backend example_int if { req_ssl_sni -i example.int }
# use_backend foo_int if { req_ssl_sni -i foo.int }
# use_backend bar_int if { req_ssl_sni -i bar.int }
# sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
backend https_daniel_steiger_ch
mode tcp
balance roundrobin
server web0 127.0.0.1:17005 send-proxy-v2
backend https_henri_bernhard_ch
mode tcp
balance roundrobin
server web0 127.0.0.1:17007 send-proxy-v2
backend foo_int
balance roundrobin
server web1 127.0.0.1:17005 send-proxy
backend bar_int
balance roundrobin
server web2 127.0.0.1:17005 ##send-proxy
backend openssh
mode tcp
# option tcplog
# option tcp-check
# tcp-check expect string SSH-2.0-
timeout server 3h
# server openssh 127.0.0.1:22 check
server openssh 127.0.0.1:22
You might also be able to use nginx instead of haproxy. Someone posted a solution here: https://serverfault.com/questions/1049158/nginx-how-to-combine-ssl-preread-protocol-with-ssl-preread-server-name-ssh-mul I didn't test that, so I'm not sure if that works, however. But according to the answering person, it works.
Then you'll need the KestrelOptionsSetup.cs
namespace HomepageHenri
{
internal class KestrelOptionsSetup
: Microsoft.Extensions.Options.IConfigureOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>
{
protected readonly Microsoft.Extensions.Logging.ILogger<KestrelOptionsSetup> m_logger;
protected System.IO.FileSystemWatcher m_watcher;
public KestrelOptionsSetup(Microsoft.Extensions.Logging.ILogger<KestrelOptionsSetup> logger)
{
this.m_logger = logger;
this.m_watcher = new System.IO.FileSystemWatcher();
} // End Constructor
~KestrelOptionsSetup()
{
if (this.m_watcher != null)
{
this.m_watcher.Dispose();
this.m_watcher = null;
} // End if (this.m_watcher != null)
} // End Destructor
void Microsoft.Extensions.Options.IConfigureOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>.Configure(
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions options)
{
options.AddServerHeader = false;
// with HAproxy, it must be here ?
options.ConfigureEndpointDefaults(Configuration.Kestrel.Https.ConfigureEndpointDefaults);
options.ConfigureHttpsDefaults(
delegate (Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions listenOptions)
{
Configuration.Kestrel.Https.HttpsDefaults(options, listenOptions, this.m_watcher);
}
);
// with NGINX, it must be HERE:
// options.ConfigureEndpointDefaults(Configuration.Kestrel.Https.ConfigureEndpointDefaults);
} // End Sub Configure
} // End Class KestrelOptionsSetup
} // End Namespace TestApplicationHttps
And the https-configuration with ServerCertificateSelector.
using Microsoft.AspNetCore.Connections; // for listenOptions.Use
using Microsoft.Extensions.DependencyInjection; // for GetRequiredService
namespace HomepageHenri.Configuration.Kestrel
{
// https://github.com/ffMathy/FluffySpoon.AspNet.EncryptWeMust
public static class Https
{
public static void ConfigureEndpointDefaults(Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions)
{
Microsoft.Extensions.Logging.ILogger<Program> logger =
listenOptions.ApplicationServices.GetRequiredService<Microsoft.Extensions.Logging.ILogger<Program>>();
listenOptions.Use(async (connectionContext, next) =>
{
await ProxyProtocol.ProxyProtocol.ProcessAsync(connectionContext, next, logger);
});
} // End Sub ConfigureEndpointDefaults
public static void HttpsDefaults(
Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions kestrelOptions
, Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions listenOptions
, System.IO.FileSystemWatcher watcher
)
{
bool isNotWindows = !System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
LetsEncryptCertificateService certificateService =
kestrelOptions.ApplicationServices.GetRequiredService<LetsEncryptCertificateService>();
// watcher.Filters.Add("localhost.yml");
// watcher.Filters.Add("example.com.yaml");
// watcher.Filters.Add("sub.example.com.yaml");
System.IO.FileSystemEventHandler onChange = delegate (object sender, System.IO.FileSystemEventArgs e)
{
CertificateFileChanged(certificateService.SniCertificateStore, sender, e);
};
watcher.Changed += new System.IO.FileSystemEventHandler(onChange);
watcher.Created += new System.IO.FileSystemEventHandler(onChange);
watcher.Deleted += new System.IO.FileSystemEventHandler(onChange);
// watcher.Renamed += new System.IO.RenamedEventHandler(OnRenamed);
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
watcher.EnableRaisingEvents = true;
listenOptions.ServerCertificateSelector =
delegate (Microsoft.AspNetCore.Connections.ConnectionContext connectionContext, string name)
{
return ServerCertificateSelector(certificateService.SniCertificateStore, connectionContext, name);
};
#if NO_NGINX_FUCKUP
listenOptions.OnAuthenticate =
delegate (Microsoft.AspNetCore.Connections.ConnectionContext connectionContext, System.Net.Security.SslServerAuthenticationOptions sslOptions)
{
// not supported on Windoze
if (isNotWindows)
{
sslOptions.CipherSuitesPolicy = new System.Net.Security.CipherSuitesPolicy(
new System.Net.Security.TlsCipherSuite[]
{
System.Net.Security.TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
System.Net.Security.TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
System.Net.Security.TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,
// ...
});
} // End if (!isWindows)
} // End Delegate
; // End OnAuthenticate
#endif
} // End Sub HttpsDefaults
public static void CertificateFileChanged(
System.Collections.Concurrent.ConcurrentDictionary<string, LetsEncryptData> certs
, object sender
, System.IO.FileSystemEventArgs e
)
{
System.Console.WriteLine(e.FullPath.ToString() + " is changed!");
// TODO: Swap certificate...
// certs["localhost"] = localhostCert;
} // End Sub CertificateFileChanged
public static System.Security.Cryptography.X509Certificates.X509Certificate2 ServerCertificateSelector(
System.Collections.Concurrent.ConcurrentDictionary<string, LetsEncryptData> certs
, Microsoft.AspNetCore.Connections.ConnectionContext connectionContext
, string name)
{
if (certs != null && certs.Count > 0)
{
if (string.IsNullOrEmpty(name))
{
System.Net.IPEndPoint ipe = (System.Net.IPEndPoint)connectionContext.LocalEndPoint;
if (ipe.Address.IsIPv4MappedToIPv6)
name = ipe.Address.MapToIPv4().ToString();
else
name = ipe.Address.ToString();
}
if (certs.ContainsKey(name))
return certs[name].Certificate;
foreach (var kvp in certs)
{
string altname = kvp.Key;
// https://serverfault.com/questions/104160/wildcard-ssl-certificate-for-second-level-subdomain/946120
// According to the RFC 6125, only a single wildcard is allowed in the most left fragment.
// Valid:
// - *.sub.domain.tld
// - *.domain.tld
// Invalid:
// sub.*.domain.tld
// *.*.domain.tld
// domain.*
// *.tld
// f*.com
// sub.*.*
// Also, note that
// *.domain.com does not cover domain.com
if (altname.StartsWith("*"))
{
altname = altname.Substring(1); // .foo.int from *.foo.int
if (name.EndsWith(altname, System.StringComparison.InvariantCultureIgnoreCase))
return kvp.Value.Certificate;
altname = altname.Substring(1); // foo.int from *.foo.int
if (altname.Equals(name, System.StringComparison.InvariantCultureIgnoreCase))
return kvp.Value.Certificate;
}
}
// throw new System.IO.InvalidDataException("No certificate for name \"" + name + "\".");
return null;
} // End if (certs != null && certs.Count > 0)
throw new System.IO.InvalidDataException("No certificate for name \"" + name + "\".");
} // End Function ServerCertificateSelector
} // End Class Https
} // End Namespace HomepageHenri.Configuration.Kestrel
And also, you'll need to add the letsencryptcertificate service, i think before your startup.cs is executed.
e.g. serviceCollection.AddSingleton
Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting; // for ConfigureWebHostDefaults, Run
using Microsoft.AspNetCore.Hosting; // for UseLinuxTransport, UseIISIntegration, UseKestrel, UseStartup
using Microsoft.AspNetCore.Connections; // for listenOptions.Use
namespace HomepageHenri
{
public class Program
{
// https://localhost:5005/
// http://localhost:5006/.well-known/acme-challenge/token.txt
// http://daniel-steiger.ch/.well-known/acme-challenge/token.txt
// https://daniel-steiger.ch/.well-known/acme-challenge/token.txt
// https://letsencrypt.org/docs/staging-environment/
public static void Main(string[] args)
{
// CreateHostBuilder(args).Build().Run();
// ln -s /etc/nginx/sites-available/example.int example.int
// cat cert.pem ca.pem > fullchain.pem
// cat ./obelix.pem ./../skynet/skynet.crt > fullchain.pem
using (System.IO.FileSystemWatcher watcher = new System.IO.FileSystemWatcher())
{
// listenOptions.UseHttps("testCert.pfx", "testPassword");
watcher.NotifyFilter = System.IO.NotifyFilters.Size
| System.IO.NotifyFilters.LastWrite
| System.IO.NotifyFilters.CreationTime
| System.IO.NotifyFilters
.FileName // Needed if text-file is changed with Visual Studio ...
;
CreateHostBuilder(args, watcher).Build().Run();
} // End Using watcher
}
//public static IHostBuilder CreateHostBuilder(string[] args) =>
// Host.CreateDefaultBuilder(args)
// .ConfigureWebHostDefaults(webBuilder =>
// {
// webBuilder.UseStartup<Startup>();
// });
// https://github.com/dotnet/aspnetcore/discussions/28238
// https://github.com/aspnet/KestrelHttpServer/issues/2103
// https://ayende.com/blog/181281-A/building-a-lets-encrypt-acme-v2-client
// https://weblog.west-wind.com/posts/2016/feb/22/using-lets-encrypt-with-iis-on-windows
// https://medium.com/@MaartenSikkema/automatically-request-and-use-lets-encrypt-certificates-in-dotnet-core-9d0d152a59b5
// https://github.com/dotnet/aspnetcore/issues/1190
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-5.0#code-try-30
public static Microsoft.Extensions.Hosting.IHostBuilder CreateHostBuilder(
string[] args, System.IO.FileSystemWatcher watcher)
{
// Microsoft.AspNetCore.Server.IIS
return Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(
delegate (Microsoft.AspNetCore.Hosting.IWebHostBuilder webBuilder)
{
// https://developers.redhat.com/blog/2018/07/24/improv-net-core-kestrel-performance-linux/
webBuilder.UseLinuxTransport();
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime
.InteropServices.OSPlatform.Windows))
{
webBuilder.UseIISIntegration();
}
else webBuilder.UseKestrel(
delegate (Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions options)
{
options.AddServerHeader = false;
}
);
webBuilder.UseUrls();
webBuilder.UseStartup<Startup>();
}) // End ConfigureWebHostDefaults
// These get created on build in CreateHostBuilder
.ConfigureHostConfiguration(delegate (IConfigurationBuilder builder)
{
// https://codingblast.com/asp-net-core-2-preview/
// https://andrewlock.net/5-ways-to-set-the-urls-for-an-aspnetcore-app/
builder.AddJsonFile("hosting.json", optional: true, reloadOnChange: true);
string launchSettings = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory()
, "Properties", "launchSettings.json");
builder.AddJsonFile(launchSettings, optional: true);
})
.ConfigureServices(delegate (HostBuilderContext context, IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<LetsEncryptCertificateService>();
});
} // End Function CreateHostBuilder
} // End Class Program
} // End Namespace HomepageHenri
With LetsEncryptCertificateService.cs
using Certes; // Dns, http, toPem, etc.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
namespace HomepageHenri
{
public class LetsEncryptCertificateService
{
public System.Uri LetsEncryptContext;
public System.Collections.Concurrent.ConcurrentDictionary<string, LetsEncryptData> SniCertificateStore;
public string AccountFileName
{
get
{
string domain = "LetsEncryptAccount";
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncryptStagingV2)
return string.Concat(domain, "_staging_v2");
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncryptV2)
return string.Concat(domain, "_v2");
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncryptStaging)
return string.Concat(domain, "_staging_v1");
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncrypt)
return string.Concat(domain, "_v1");
return string.Concat(domain, "_undefined");
}
} // End Property AccountFileName
public string GetCertificateFileName(string domain)
{
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncryptStagingV2)
return string.Concat(domain, "_staging_v2");
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncryptV2)
return string.Concat(domain, "_v2");
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncryptStaging)
return string.Concat(domain, "_staging_v1");
if (this.LetsEncryptContext == Certes.Acme.WellKnownServers.LetsEncrypt)
return string.Concat(domain, "_v1");
return string.Concat(domain, "_undefined");
} // End Function GetCertificateFileName
public LetsEncryptCertificateService(
Microsoft.AspNetCore.Hosting.IWebHostEnvironment env
//, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions> options
)
{
bool isDev = Microsoft.Extensions.Hosting.HostEnvironmentEnvExtensions.IsDevelopment(env);
// Prodesk for testing on production machine
if ("prodesk".Equals(System.Environment.MachineName, System.StringComparison.InvariantCultureIgnoreCase))
{
isDev = false;
}
bool override_dev = false;
if (isDev || override_dev)
this.LetsEncryptContext = Certes.Acme.WellKnownServers.LetsEncryptStagingV2;
else
this.LetsEncryptContext = Certes.Acme.WellKnownServers.LetsEncryptV2;
this.SniCertificateStore = new System.Collections.Concurrent.ConcurrentDictionary<string, LetsEncryptData>(
System.StringComparer.OrdinalIgnoreCase
);
// string pfxBase64 = SecretManager.GetSecret<string>("henri-bernhard.ch");
// byte[] pfx = System.Convert.FromBase64String(pfxBase64);
string[] letsEncryptDomains = new string[] {
"henri-bernhard.ch"
, "www.henri-bernhard.ch"
};
string[] altNames = SelfSigned.GetAlternativeNames(isDev ? letsEncryptDomains : new string[0]);
byte[] pfx = null;
try
{
pfx = SelfSigned.CreateSelfSignedCertificate(altNames, LetsEncryptData.DEFAULT_PASSWORD);
}
catch (System.Exception ex)
{
// if SSL Handled by IIS
if (System.Environment.GetEnvironmentVariable("APP_POOL_ID") is string)
return;
System.Console.WriteLine(ex.Message);
System.Console.WriteLine(ex.StackTrace);
System.Environment.Exit(666);
}
foreach (string altName in altNames)
{
LetsEncryptData certData = new LetsEncryptData(altName, false);
this.SniCertificateStore[altName] = certData.FromPfx(pfx, certData.PfxPassword);
// this.SniCertificateStore[altName] = certData.FromPem(cert, key);
} // Next altName
if (!isDev || override_dev)
{
foreach (string thisDomain in letsEncryptDomains)
{
this.SniCertificateStore[thisDomain] = new LetsEncryptData(thisDomain, true);
} // Next thisDomain
} // End if (!isDev)
// string cert = SecretManager.GetSecret<string>("ssl_cert");
// string key = SecretManager.GetSecret<string>("ssl_key");
// LetsEncryptData henri_bernhard_ch = new LetsEncryptData();
// this.SniCertificateStore["henri-bernhard"] = henri_bernhard_ch.FromPfx(pfx);
} // End Constructor
protected async System.Threading.Tasks.Task<Certes.AcmeContext> CreateAccountAsync(string domain, string accountMail)
{
if (accountMail == null)
throw new System.ArgumentNullException(nameof(accountMail));
Certes.AcmeContext acme = new Certes.AcmeContext(this.LetsEncryptContext);
Certes.Acme.IAccountContext account = await acme.NewAccount(accountMail, true);
// Save the account key for later use
string pemKey = acme.AccountKey.ToPem();
// System.IO.File.WriteAllText(domain + ".account", pemKey, new System.Text.UTF8Encoding(false));
SecretManager.SetSecret("All", this.AccountFileName, pemKey);
return acme;
} // End Task CreateAccountAsync
protected async System.Threading.Tasks.Task<Certes.AcmeContext> CreateAccountAsync(string domain)
{
string accountMail = "webmaster@" + domain;
return await CreateAccountAsync(domain, accountMail);
} // End Task CreateAccountAsync
protected async System.Threading.Tasks.Task<Certes.AcmeContext> GetContextAsync(string pemKey)
{
Certes.IKey accountKey = Certes.KeyFactory.FromPem(pemKey);
Certes.AcmeContext acme = new Certes.AcmeContext(this.LetsEncryptContext, accountKey);
// Certes.Acme.IAccountContext account = await acme.Account();
await System.Threading.Tasks.Task.CompletedTask; // Calm Warnings
return acme;
} // End Task UseExistingAccount
public async System.Threading.Tasks.Task<Certes.AcmeContext> GetOrCreateContextAsync(string domain)
{
string pem = SecretManager.GetSecret<string>("All", this.AccountFileName);
if (pem != null)
{
return await GetContextAsync(pem);
}
Certes.AcmeContext context = await CreateAccountAsync(domain);
// System.Uri tos = await context.TermsOfService();
return context;
} // End Task GetOrCreateContextAsync
protected async System.Threading.Tasks.Task<Certes.Acme.IOrderContext> NewWildcardOrderAsync(
Certes.AcmeContext acme, string domain = "*.your.domain.name")
{
Certes.Acme.IOrderContext order = await acme.NewOrder(new[] { domain });
Certes.Acme.IAuthorizationContext authz = System.Linq.Enumerable.First(await order.Authorizations());
Certes.Acme.IChallengeContext dnsChallenge = await authz.Dns();
string dnsTxt = acme.AccountKey.DnsTxt(dnsChallenge.Token);
return order;
} // End Task NewWildcardOrder
protected async System.Threading.Tasks.Task<Certes.Acme.IOrderContext> NewNonWildCardOrderAsync(
Certes.AcmeContext acme, string domain = "your.domain.name")
{
Certes.Acme.IOrderContext order = await acme.NewOrder(new string[] { domain });
return order;
} // End Task NewNonWildCardOrder
// https://github.com/fszlin/certes/issues/77
protected async System.Threading.Tasks.Task<Certes.Acme.IChallengeContext> GetHttpChallengeAsync(
Certes.Acme.IOrderContext order)
{
Certes.Acme.IAuthorizationContext authz = System.Linq.Enumerable.First(await order.Authorizations());
Certes.Acme.IChallengeContext httpChallenge = await authz.Http();
// string keyAuthz = httpChallenge.KeyAuthz;
return httpChallenge;
} // End Task GetToken
// https://github.com/fszlin/certes/issues/186
protected async System.Threading.Tasks.Task<Certes.Acme.IChallengeContext> GetDnsChallengeAsync(
Certes.Acme.IOrderContext order)
{
Certes.Acme.IAuthorizationContext authz = System.Linq.Enumerable.First(await order.Authorizations());
Certes.Acme.IChallengeContext dnsChallenge = await authz.Dns();
return dnsChallenge;
}
protected async System.Threading.Tasks.Task<Certes.Acme.Resource.Challenge> ValidateAsync(
Certes.Acme.IChallengeContext challenge)
{
Certes.Acme.Resource.Challenge val = await challenge.Validate();
return val;
} // End Task Validate
protected async System.Threading.Tasks.Task DownloadCertificateAsync(
Certes.Acme.IOrderContext order, string domain = "your.domain.name")
{
Certes.IKey privateKey = Certes.KeyFactory.NewKey(Certes.KeyAlgorithm.ES256);
Certes.Acme.CertificateChain cert = await order.Generate(new Certes.CsrInfo
{
CountryName = "CH",
State = "Bern",
Locality = "Muensingen",
Organization = "SVP Bern",
OrganizationUnit = "SVP Muensingen",
CommonName = domain,
}, privateKey);
string pk = privateKey.ToPem();
string certPem = cert.ToPem();
// System.IO.File.WriteAllText(domain + ".key", pk, new System.Text.UTF8Encoding(false));
// System.IO.File.WriteAllText(domain + ".pem", certPem, new System.Text.UTF8Encoding(false));
this.SniCertificateStore[domain].FromPem(certPem, pk);
// System.IO.File.WriteAllBytes(domain + ".pfx", pfx);
Certes.Pkcs.PfxBuilder pfxBuilder = cert.ToPfx(privateKey);
byte[] pfx = pfxBuilder.Build(domain, this.SniCertificateStore[domain].PfxPassword);
string pfxBase64 = System.Convert.ToBase64String(pfx);
string certFileName = this.GetCertificateFileName(domain);
SecretManager.SetSecret(certFileName, pfxBase64);
} // End Task DownloadCertificate
protected async System.Threading.Tasks.Task GetOrRenewCertificateAsync(string tokenFolder, string domain)
{
if (!this.SniCertificateStore.ContainsKey(domain))
this.SniCertificateStore[domain] = new LetsEncryptData(domain, true);
Certes.AcmeContext context = await GetOrCreateContextAsync(domain);
Certes.Acme.IOrderContext order = await NewNonWildCardOrderAsync(context, domain);
Certes.Acme.IChallengeContext challenge = await GetHttpChallengeAsync(order);
this.SniCertificateStore[domain].Token = challenge.Token;
this.SniCertificateStore[domain].SignedNonce = challenge.KeyAuthz;
// IChallengeContext dnsChallenge = await GetDnsChallengeAsync(order);
// string dnsTxt = context.AccountKey.DnsTxt(dnsChallenge.Token);
// LoopiaRPCCalls.createTxtRecord(dnsTxt);
// At this point _acme-challenge.xyz.se is created
// System.Threading.Thread.Sleep(60 * 1000);
// I realized this is caused by the TTL on the TXT record.
// The DNS service was most likely providing a cached version to let's encrypt.
// What I've done in the past is write code to loop doing DNS lookups
// until the correct TXT value is returned. Waiting for X minutes isn't reliable.
// string url = "http://" + domain + "/.well-known/acme-challenge/"+ challenge.Token;
// System.Console.WriteLine(url);
// string path = System.IO.Path.Combine(tokenFolder, challenge.Token);
// byte[] data = System.Text.Encoding.ASCII.GetBytes(challenge.KeyAuthz);
// System.IO.File.WriteAllText(path, ctx.KeyAuthz, System.Text.Encoding.UTF8);
// System.IO.File.WriteAllBytes(path, data);
Certes.Acme.Resource.Challenge c = await ValidateAsync(challenge);
if (c.Status == Certes.Acme.Resource.ChallengeStatus.Invalid)
{
System.Console.WriteLine(c.Error.Detail);
throw new System.Exception("Invalid Challenge", new System.Exception(c.Error.Detail));
}
else
await System.Threading.Tasks.Task.Delay(15000);
await DownloadCertificateAsync(order, domain);
// System.Console.WriteLine(this.SniCertificateStore[domain].Certificate);
// The path at which the resource is provisioned is comprised of the
// fixed prefix "/.well-known/acme-challenge/", followed by the "token"
// value in the challenge.
// The value of the resource MUST be the ASCII representation of the key authorization.
} // End Task GetOrRenewCertificateAsync
public async System.Threading.Tasks.Task GetOrRenewCertificateAsync(string domain)
{
string certFileName = this.GetCertificateFileName(domain);
string base64 = SecretManager.GetSecret<string>(certFileName);
bool doRenew = true;
if (base64 != null)
{
byte[] ba = System.Convert.FromBase64String(base64);
LetsEncryptData leData = new LetsEncryptData(domain, true);
leData.FromPfx(ba, leData.PfxPassword);
// System.Console. WriteLine(leData.Certificate.NotBefore);
// System.Console. WriteLine(leData.Certificate.NotAfter);
System.TimeSpan diff = leData.Certificate.NotAfter - System.DateTime.Now;
// System.Console. WriteLine(diff.TotalDays);
if (diff.TotalDays > 57)
{
this.SniCertificateStore[domain] = leData;
doRenew = false;
} // End if (diff.TotalDays > 57)
} // End if (base64 != null)
if (doRenew)
await GetOrRenewCertificateAsync(null, domain);
} // End Task GetOrRenewCertificateAsync
// https://security.stackexchange.com/questions/107640/how-does-letsencrypt-orgs-acme-work
// DNS Challenge:
// https://github.com/fszlin/certes/issues/186
// find port of program netstat -tulpn
// set gopath: https://stackoverflow.com/questions/21001387/how-do-i-set-the-gopath-environment-variable-on-ubuntu-what-file-must-i-edit/53026674#53026674
// https://kifarunix.com/install-and-setup-gvm-11-on-ubuntu-20-04/
// https://github.com/moovweb/gvm
// https://github.com/fszlin/certes
// https://security.stackexchange.com/questions/107640/how-does-letsencrypt-orgs-acme-work
// https://letsencrypt.org/docs/staging-environment/
// https://letsencrypt.org/docs/rate-limits/
// https://github.com/letsencrypt/pebble
// https://hub.docker.com/r/letsencrypt/pebble
// https://github.com/letsencrypt/pebble/blob/master/cmd/pebble-challtestsrv/README.md
internal static async System.Threading.Tasks.Task Test()
{
string domain = "henri-bernhard.ch";
string tokenFolder = @"/var/www/" + domain.ToLowerInvariant() + "/.well-known/acme-challenge/";
tokenFolder = System.IO.Path.Combine(@"/var/www", domain.ToLowerInvariant(), ".well-known", "acme-challenge");
if (System.Environment.OSVersion.Platform != System.PlatformID.Unix)
tokenFolder = System.IO.Path.Combine(@"D:\inetpub\www", domain.ToLowerInvariant(), ".well-known", "acme-challenge");
if (!System.IO.Directory.Exists(tokenFolder))
System.IO.Directory.CreateDirectory(tokenFolder);
Microsoft.AspNetCore.Hosting.IWebHostEnvironment environment = new MockEnvironment();
// Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions> opts =
// Microsoft.Extensions.Options.Options.Create(new Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionOptions());
LetsEncryptCertificateService acmeService = new LetsEncryptCertificateService(environment);
await acmeService.GetOrRenewCertificateAsync(tokenFolder, domain);
} // End Sub Main
} // End Class Program
public class MockEnvironment
: Microsoft.AspNetCore.Hosting.IWebHostEnvironment
{
string IWebHostEnvironment.WebRootPath { get => "./wwwroot"; set => throw new System.NotImplementedException(); }
IFileProvider IWebHostEnvironment.WebRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
string IHostEnvironment.ApplicationName { get => nameof(HomepageHenri); set => throw new System.NotImplementedException(); }
IFileProvider IHostEnvironment.ContentRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
string IHostEnvironment.ContentRootPath { get => "./"; set => throw new System.NotImplementedException(); }
string IHostEnvironment.EnvironmentName { get => "Development"; set => throw new System.NotImplementedException(); }
}
}
Reconsidering for 8. We've had multiple first party requests.
Assuming this didn't make it into 8?
Nope
Any update on this Feature Request?
One of the advantages of using haproxy over nginx is, that you don't need nginx-PLUS (commercial offering) if you want to add SNI-hosts dynamically. Another advantages of using HAproxy over nginx is that HAproxy can do SSL/TLS-passthrough forwarding. Meaning HAproxy then doesn't need the ssl certificate etc, which is great, because then I only need to configure the SSL-key on Kestrel - especially if the SSL/TLS certificate changes at runtime (LetsEncrypt).
A further advantage is, that a TCP-level-proxy will not be limited by a proxy-pipeline, e.g. interfering with HTTP 1.1/2.0.
Now, I have multiple sites running on the same IP. The way to do this with HAproxy is sni-header inspection.
But because Kestrel does not support the proxy protocol, it's impossible to get the client's IP .
GoLang has support for that since at least 4 years... By breaking the ability to get the client's IP, you're breaking the ability to geolocate - which breaks the ability to adapt content to a user's region ...
If Kestrel could be configured to accept PROXY-protocol connections, it could decapsulate the HTTP request. Since Kestrel is the route termination, it could decrypt the request, and update the "Forwarded" HTTP header (and related HTTP headers) appending any source address that is communicated using the PROXY protocol.
e.g.