SSL ON .NET CORE VM在负载平衡器后面



我当前正在设置一个高可用性(HA)环境,两台坐在标准Azure负载平衡器后面的Azure Virtual Machines。现在我知道标准负载平衡器仅是第4层,这意味着它不能进行SSL卸载。

两个VM都在运行.NET Core Web API。显然,他们每个人都需要SSL证书来处理来自负载平衡器的SSL连接。

我知道我可以购买SSL证书,然后设置Kestrel在Web API上使用证书,但我希望免费证书。我知道另一个选项是使用Nginx服务器生成证书,然后将证书跨证书复制到Web API,但这意味着我需要每3个月重复每3个月的过程,这是一个相当大的痛苦,因为这意味着我会停下时间我将HA群集离线续订。

有人知道一种使用方法的方法,让我们加密位于负载平衡器后面的两个VM上吗?

前言

好吧,所以我与上述有关。它要求我编写一个实用程序,该实用程序可以使用 dns 验证自动续订Lets Genters证书。它使用具有API的Azure DNS或其他DNS提供商非常重要,因为您需要能够通过API或其他一些界面与提供商直接修改DNS记录。

我正在使用Azure DNS,它为我管理了整个域,因此下面的代码适用于Azure DNS,但是您可以修改API以与具有某种API的任何提供者一起使用。

第二部分,我的高可用性(HA)群集中没有任何停机时间。因此,我所做的是,将证书写入数据库,然后在VM的启动时动态阅读。因此,基本上,每次Kestrel启动它都会从DB读取证书,然后使用它。


代码

数据库模型

您需要将以下模型添加到数据库中,以便您可以在某个地方存储实际的证书详细信息。

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();
        }
    }
}

好吧,这要经历很多

  1. 创建一个帐户
  2. 获取帐户密钥
  3. 为域创建新订单
  4. 通过所有组织循环
  5. 对每个人执行DNS验证
  6. 生成证书
  7. 将证书保存到DB
  8. 重新启动VM

实际的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 Integration

无论我在哪里打电话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;
}

我希望这能够帮助与我自己处于同一困境的其他人。

最新更新