负载均衡器后面的 .NET Core VMS 上的 SSL
SSL on .NET Core VMS behind load balancer
我目前正在设置一个高可用性 (HA) 环境,其中有两个 Azure 虚拟机 运行 Ubuntu 位于标准 Azure 负载均衡器后面。现在我知道标准负载均衡器只有第 4 层,这意味着它不能进行 SSL 卸载。
这两个 VM 都是 运行 .NET Core Web API。他们显然都需要 SSL 证书来处理来自负载均衡器的 SSL 连接。
我知道我可以购买 SSL 证书并设置 Kestrel 以在 Web API 本身上使用该证书,但我想要免费证书。我知道另一种选择是使用 nginx 服务器生成证书,然后将证书复制到 Web API 但这意味着我需要每 3 个月重复一次该过程,这非常麻烦,因为这意味着我当我使 HA 集群脱机以更新证书时会有停机时间。
有谁知道在负载均衡器后面的两个虚拟机上使用 Lets Encrypt 的方法吗?
前言
好吧,我上面说的是对的。它要求我编写一个实用程序,使用 DNS 验证自动更新我的 Lets Encrypt 证书。它使用 Azure DNS 或具有 API 的其他 DNS 提供商非常重要,因为您需要能够使用 API 或与您的提供商的其他接口直接修改您的 DNS 记录。
我正在使用 Azure DNS,它为我管理整个域,因此下面的代码适用于 Azure DNS,但您可以修改 API 以与您选择的具有某种形式的任何提供商合作API.
第二部分是在我的高可用性 (HA) 集群中不要有任何停机时间。所以我所做的是,将证书写入数据库,然后在我的 VM 启动时动态读取它。所以基本上每次 Kestrel 启动时,它都会从数据库中读取证书,然后使用它。
代码
数据库模型
您需要将以下模型添加到您的数据库中,以便您可以在某处存储实际的证书详情。
public class Certificate
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string FullChainPem { get; set; }
public string CertificatePfx { get; set; }
public string CertificatePassword { get; set; }
public DateTime CertificateExpiry { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
创建模型后,您需要将其放置在上下文中,如下所示:
public DbSet<Certificate> Certificates { get; set; }
应用服务器
在您的应用程序服务器上,您可能希望使用 Kestrel
作为 Web 服务器,然后从数据库动态加载证书。因此,将以下内容添加到您的 CreateWebHostBuilder
方法中。重要的是,这是在 .UseStartup<Startup>()
之后
.UseKestrel(opt = >{
//Get the application services
var applicationServices = opt.ApplicationServices;
//Create and use scope
using(var scope = applicationServices.CreateScope()) {
//Get the database context to work with
var context = scope.ServiceProvider.GetService < DBContext > ();
//Get the certificate
var certificate = context.Certificates.Last();
var pfxBytes = Convert.FromBase64String(certificate.CertificatePfx);
var pfxPassword = certificate.CertificatePassword;
//Create the certificate
var cert = new X509Certificate2(pfxBytes, pfxPassword);
//Listen on the specified IP and port
opt.Listen(IPAddress.Any, 443, listenOpts = >{
//Use HTTPS
listenOpts.UseHttps(cert);
});
}
});
让加密实用程序
这就是解决方案的核心。它处理证书请求、质询、DNS 验证以及证书的存储。它还将自动重启 Azure 中使用证书的每个 VM 实例,以便它们提取新证书。
Main
逻辑如下,会检查证书是否需要更新。
static void Main(string[] args) {
while (true) {
//Get the latest certificate in the DB for the servers
var lastCertificate = _db.Certificates.LastOrDefault();
//Check if the expiry date of last certificate is more than a month away
if (lastCertificate != null && (lastCertificate.CertificateExpiry - DateTime.Now).TotalDays > 31) {
//Log out some info
Console.WriteLine($ "[{DateTime.Now}] - Certificate still valid, sleeping for a day.");
//Sleep the thread
Thread.Sleep(TimeSpan.FromDays(1));
}
else {
//Renew the certificates
RenewCertificates();
}
}
}
好吧,这是很多事情,但如果你把它分解的话,其实很简单
- 创建帐户
- 获取账户密钥
- 为域创建新订单
- 遍历所有组织
- 对它们中的每一个执行 DNS 验证
- 生成证书
- 将证书保存到数据库
- 重启虚拟机
实际RenewCertificates
方法如下:
/// <summary>
/// Method that will renew the domain certificates and update the database with them
/// </summary>
public static void RenewCertificates() {
Console.WriteLine($ "[{DateTime.Now}] - Starting certificate renewal.");
//Instantiate variables
AcmeContext acme;
IAccountContext account;
//Try and get the setting value for ACME Key
var acmeKey = _db.Settings.FirstOrDefault(s = >s.Key == "ACME");
//Check if acme key is null
if (acmeKey == null) {
//Set the ACME servers to use
#if DEBUG
acme = new AcmeContext(WellKnownServers.LetsEncryptStagingV2);
#else
acme = new AcmeContext(WellKnownServers.LetsEncryptV2);
#endif
//Create the new account
account = acme.NewAccount("yourname@yourdomain.tld", true).Result;
//Save the key to the DB to be used
_db.Settings.Add(new Setting {
Key = "ACME",
Value = acme.AccountKey.ToPem()
});
//Save DB changes
_db.SaveChanges();
}
else {
//Get the account key from PEM
var accountKey = KeyFactory.FromPem(acmeKey.Value);
//Set the ACME servers to use
#if DEBUG
acme = new AcmeContext(WellKnownServers.LetsEncryptStagingV2, accountKey);
#else
acme = new AcmeContext(WellKnownServers.LetsEncryptV2, accountKey);
#endif
//Get the actual account
account = acme.Account().Result;
}
//Create an order for wildcard domain and normal domain
var order = acme.NewOrder(new[] {
"*.yourdomain.tld",
"yourdomain.tld"
}).Result;
//Generate the challenges for the domains
var authorizations = order.Authorizations().Result;
//Error flag
var hasFailed = false;
foreach(var authorization in authorizations) {
//Get the DNS challenge for the authorization
var dnsChallenge = authorization.Dns().Result;
//Get the DNS TXT
var dnsTxt = acme.AccountKey.DnsTxt(dnsChallenge.Token);
Console.WriteLine($ "[{DateTime.Now}] - Received DNS challenge data.");
//Set the DNS record
Azure.SetAcmeTxtRecord(dnsTxt);
Console.WriteLine($ "[{DateTime.Now}] - Updated DNS challenge data.");
Console.WriteLine($ "[{DateTime.Now}] - Waiting 1 minute before checking status.");
dnsChallenge.Validate();
//Wait 1 minute
Thread.Sleep(TimeSpan.FromMinutes(1));
//Check the DNS challenge
var valid = dnsChallenge.Validate().Result;
//If the verification fails set failed flag
if (valid.Status != ChallengeStatus.Valid) hasFailed = true;
}
//Check whether challenges failed
if (hasFailed) {
Console.WriteLine($ "[{DateTime.Now}] - DNS challenge(s) failed, retrying.");
//Recurse
RenewCertificates();
return;
}
else {
Console.WriteLine($ "[{DateTime.Now}] - DNS challenge(s) successful.");
//Generate a private key
var privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
//Generate certificate
var cert = order.Generate(new CsrInfo {
CountryName = "ZA",
State = "Gauteng",
Locality = "Pretoria",
Organization = "Your Organization",
OrganizationUnit = "Production",
},
privateKey).Result;
Console.WriteLine($ "[{DateTime.Now}] - Certificate generated successfully.");
//Get the full chain
var fullChain = cert.ToPem();
//Generate password
var pass = Guid.NewGuid().ToString();
//Export the pfx
var pfxBuilder = cert.ToPfx(privateKey);
var pfx = pfxBuilder.Build("yourdomain.tld", pass);
//Create database entry
_db.Certificates.Add(new Certificate {
FullChainPem = fullChain,
CertificatePfx = Convert.ToBase64String(pfx),
CertificatePassword = pass,
CertificateExpiry = DateTime.Now.AddMonths(2)
});
//Save changes
_db.SaveChanges();
Console.WriteLine($ "[{DateTime.Now}] - Database updated with new certificate.");
Console.WriteLine($ "[{DateTime.Now}] - Restarting VMs.");
//Restart the VMS
Azure.RestartAllVms();
}
}
Azure 集成
无论我在哪里调用 Azure
,您都需要编写 API 包装器来设置 DNS TXT 记录,然后才能从您的托管服务提供商处重新启动 VM。我的全部使用 Azure,所以操作起来非常简单。这是 Azure 代码:
/// <summary>
/// Method that will set the TXT record value of the ACME challenge
/// </summary>
/// <param name="txtValue">Value for the TXT record</param>
/// <returns>Whether call was successful or not</returns>
public static bool SetAcmeTxtRecord(string txtValue) {
//Set the zone endpoint
const string url = "https://management.azure.com/subscriptions/{subId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/dnsZones/{dnsZone}/txt/_acme-challenge?api-version=2018-03-01-preview";
//Authenticate API
AuthenticateApi();
//Build up the body to put
var body = $ "{{\"properties\": {{\"metadata\": {{}},\"TTL\": 225,\"TXTRecords\": [{{\"value\": [\"{txtValue}\"]}}]}}}}";
//Build up the string content
var content = new StringContent(body, Encoding.UTF8, "application/json");
//Create the response
var response = client.PutAsync(url, content).Result;
//Return the response
return response.IsSuccessStatusCode;
}
我希望这能够帮助到和我有同样困境的其他人。
我目前正在设置一个高可用性 (HA) 环境,其中有两个 Azure 虚拟机 运行 Ubuntu 位于标准 Azure 负载均衡器后面。现在我知道标准负载均衡器只有第 4 层,这意味着它不能进行 SSL 卸载。
这两个 VM 都是 运行 .NET Core Web API。他们显然都需要 SSL 证书来处理来自负载均衡器的 SSL 连接。
我知道我可以购买 SSL 证书并设置 Kestrel 以在 Web API 本身上使用该证书,但我想要免费证书。我知道另一种选择是使用 nginx 服务器生成证书,然后将证书复制到 Web API 但这意味着我需要每 3 个月重复一次该过程,这非常麻烦,因为这意味着我当我使 HA 集群脱机以更新证书时会有停机时间。
有谁知道在负载均衡器后面的两个虚拟机上使用 Lets Encrypt 的方法吗?
前言
好吧,我上面说的是对的。它要求我编写一个实用程序,使用 DNS 验证自动更新我的 Lets Encrypt 证书。它使用 Azure DNS 或具有 API 的其他 DNS 提供商非常重要,因为您需要能够使用 API 或与您的提供商的其他接口直接修改您的 DNS 记录。
我正在使用 Azure DNS,它为我管理整个域,因此下面的代码适用于 Azure DNS,但您可以修改 API 以与您选择的具有某种形式的任何提供商合作API.
第二部分是在我的高可用性 (HA) 集群中不要有任何停机时间。所以我所做的是,将证书写入数据库,然后在我的 VM 启动时动态读取它。所以基本上每次 Kestrel 启动时,它都会从数据库中读取证书,然后使用它。
代码
数据库模型
您需要将以下模型添加到您的数据库中,以便您可以在某处存储实际的证书详情。
public class Certificate
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string FullChainPem { get; set; }
public string CertificatePfx { get; set; }
public string CertificatePassword { get; set; }
public DateTime CertificateExpiry { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
创建模型后,您需要将其放置在上下文中,如下所示:
public DbSet<Certificate> Certificates { get; set; }
应用服务器
在您的应用程序服务器上,您可能希望使用 Kestrel
作为 Web 服务器,然后从数据库动态加载证书。因此,将以下内容添加到您的 CreateWebHostBuilder
方法中。重要的是,这是在 .UseStartup<Startup>()
.UseKestrel(opt = >{
//Get the application services
var applicationServices = opt.ApplicationServices;
//Create and use scope
using(var scope = applicationServices.CreateScope()) {
//Get the database context to work with
var context = scope.ServiceProvider.GetService < DBContext > ();
//Get the certificate
var certificate = context.Certificates.Last();
var pfxBytes = Convert.FromBase64String(certificate.CertificatePfx);
var pfxPassword = certificate.CertificatePassword;
//Create the certificate
var cert = new X509Certificate2(pfxBytes, pfxPassword);
//Listen on the specified IP and port
opt.Listen(IPAddress.Any, 443, listenOpts = >{
//Use HTTPS
listenOpts.UseHttps(cert);
});
}
});
让加密实用程序
这就是解决方案的核心。它处理证书请求、质询、DNS 验证以及证书的存储。它还将自动重启 Azure 中使用证书的每个 VM 实例,以便它们提取新证书。
Main
逻辑如下,会检查证书是否需要更新。
static void Main(string[] args) {
while (true) {
//Get the latest certificate in the DB for the servers
var lastCertificate = _db.Certificates.LastOrDefault();
//Check if the expiry date of last certificate is more than a month away
if (lastCertificate != null && (lastCertificate.CertificateExpiry - DateTime.Now).TotalDays > 31) {
//Log out some info
Console.WriteLine($ "[{DateTime.Now}] - Certificate still valid, sleeping for a day.");
//Sleep the thread
Thread.Sleep(TimeSpan.FromDays(1));
}
else {
//Renew the certificates
RenewCertificates();
}
}
}
好吧,这是很多事情,但如果你把它分解的话,其实很简单
- 创建帐户
- 获取账户密钥
- 为域创建新订单
- 遍历所有组织
- 对它们中的每一个执行 DNS 验证
- 生成证书
- 将证书保存到数据库
- 重启虚拟机
实际RenewCertificates
方法如下:
/// <summary>
/// Method that will renew the domain certificates and update the database with them
/// </summary>
public static void RenewCertificates() {
Console.WriteLine($ "[{DateTime.Now}] - Starting certificate renewal.");
//Instantiate variables
AcmeContext acme;
IAccountContext account;
//Try and get the setting value for ACME Key
var acmeKey = _db.Settings.FirstOrDefault(s = >s.Key == "ACME");
//Check if acme key is null
if (acmeKey == null) {
//Set the ACME servers to use
#if DEBUG
acme = new AcmeContext(WellKnownServers.LetsEncryptStagingV2);
#else
acme = new AcmeContext(WellKnownServers.LetsEncryptV2);
#endif
//Create the new account
account = acme.NewAccount("yourname@yourdomain.tld", true).Result;
//Save the key to the DB to be used
_db.Settings.Add(new Setting {
Key = "ACME",
Value = acme.AccountKey.ToPem()
});
//Save DB changes
_db.SaveChanges();
}
else {
//Get the account key from PEM
var accountKey = KeyFactory.FromPem(acmeKey.Value);
//Set the ACME servers to use
#if DEBUG
acme = new AcmeContext(WellKnownServers.LetsEncryptStagingV2, accountKey);
#else
acme = new AcmeContext(WellKnownServers.LetsEncryptV2, accountKey);
#endif
//Get the actual account
account = acme.Account().Result;
}
//Create an order for wildcard domain and normal domain
var order = acme.NewOrder(new[] {
"*.yourdomain.tld",
"yourdomain.tld"
}).Result;
//Generate the challenges for the domains
var authorizations = order.Authorizations().Result;
//Error flag
var hasFailed = false;
foreach(var authorization in authorizations) {
//Get the DNS challenge for the authorization
var dnsChallenge = authorization.Dns().Result;
//Get the DNS TXT
var dnsTxt = acme.AccountKey.DnsTxt(dnsChallenge.Token);
Console.WriteLine($ "[{DateTime.Now}] - Received DNS challenge data.");
//Set the DNS record
Azure.SetAcmeTxtRecord(dnsTxt);
Console.WriteLine($ "[{DateTime.Now}] - Updated DNS challenge data.");
Console.WriteLine($ "[{DateTime.Now}] - Waiting 1 minute before checking status.");
dnsChallenge.Validate();
//Wait 1 minute
Thread.Sleep(TimeSpan.FromMinutes(1));
//Check the DNS challenge
var valid = dnsChallenge.Validate().Result;
//If the verification fails set failed flag
if (valid.Status != ChallengeStatus.Valid) hasFailed = true;
}
//Check whether challenges failed
if (hasFailed) {
Console.WriteLine($ "[{DateTime.Now}] - DNS challenge(s) failed, retrying.");
//Recurse
RenewCertificates();
return;
}
else {
Console.WriteLine($ "[{DateTime.Now}] - DNS challenge(s) successful.");
//Generate a private key
var privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256);
//Generate certificate
var cert = order.Generate(new CsrInfo {
CountryName = "ZA",
State = "Gauteng",
Locality = "Pretoria",
Organization = "Your Organization",
OrganizationUnit = "Production",
},
privateKey).Result;
Console.WriteLine($ "[{DateTime.Now}] - Certificate generated successfully.");
//Get the full chain
var fullChain = cert.ToPem();
//Generate password
var pass = Guid.NewGuid().ToString();
//Export the pfx
var pfxBuilder = cert.ToPfx(privateKey);
var pfx = pfxBuilder.Build("yourdomain.tld", pass);
//Create database entry
_db.Certificates.Add(new Certificate {
FullChainPem = fullChain,
CertificatePfx = Convert.ToBase64String(pfx),
CertificatePassword = pass,
CertificateExpiry = DateTime.Now.AddMonths(2)
});
//Save changes
_db.SaveChanges();
Console.WriteLine($ "[{DateTime.Now}] - Database updated with new certificate.");
Console.WriteLine($ "[{DateTime.Now}] - Restarting VMs.");
//Restart the VMS
Azure.RestartAllVms();
}
}
Azure 集成
无论我在哪里调用 Azure
,您都需要编写 API 包装器来设置 DNS TXT 记录,然后才能从您的托管服务提供商处重新启动 VM。我的全部使用 Azure,所以操作起来非常简单。这是 Azure 代码:
/// <summary>
/// Method that will set the TXT record value of the ACME challenge
/// </summary>
/// <param name="txtValue">Value for the TXT record</param>
/// <returns>Whether call was successful or not</returns>
public static bool SetAcmeTxtRecord(string txtValue) {
//Set the zone endpoint
const string url = "https://management.azure.com/subscriptions/{subId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/dnsZones/{dnsZone}/txt/_acme-challenge?api-version=2018-03-01-preview";
//Authenticate API
AuthenticateApi();
//Build up the body to put
var body = $ "{{\"properties\": {{\"metadata\": {{}},\"TTL\": 225,\"TXTRecords\": [{{\"value\": [\"{txtValue}\"]}}]}}}}";
//Build up the string content
var content = new StringContent(body, Encoding.UTF8, "application/json");
//Create the response
var response = client.PutAsync(url, content).Result;
//Return the response
return response.IsSuccessStatusCode;
}
我希望这能够帮助到和我有同样困境的其他人。