SSL на .NET Core VMS за балансировщиком нагрузки - PullRequest
0 голосов
/ 07 марта 2019

В настоящее время я настраиваю среду высокой доступности (HA) с двумя виртуальными машинами Azure, работающими под управлением Ubuntu, за стандартным балансировщиком нагрузки Azure. Теперь я знаю, что стандартным балансировщиком нагрузки является только уровень 4, что означает, что он не может выполнять разгрузку SSL.

На двух виртуальных машинах работает .NET Core Web API. Каждый из них, очевидно, будет нуждаться в SSL-сертификате для обработки SSL-соединений, поступающих от балансировщика нагрузки.

Я знаю, что могу купить сертификат SSL и просто настроить Kestrel для использования сертификата в самом веб-API, но мне нужен бесплатный сертификат. Я знаю, что другой вариант - создать сертификат с помощью сервера nginx, а затем скопировать сертификаты в Web API, но это означает, что мне нужно будет повторять этот процесс каждые 3 месяца, что весьма затруднительно, так как это означает, что у меня было бы время простоя, пока Я перевожу кластер высокой доступности в автономный режим, чтобы обновить сертификат.

Кто-нибудь знает, как использовать Lets Encrypt на двух виртуальных машинах, расположенных за балансировщиком нагрузки?

1 Ответ

1 голос
/ 11 марта 2019

Предисловие

Хорошо, так что я пришел прямо с вышеизложенным.Мне потребовалось написать утилиту, которая автоматически обновляет мои сертификаты Lets Encrypt с использованием DNS проверки.Очень важно, чтобы он использовал Azure DNS или другого DNS-провайдера, который имеет API, так как вам нужно будет иметь возможность изменять свои записи DNS напрямую с помощью API или другого интерфейса с вашим провайдером.

Яиспользуя Azure DNS, и он управляет всем доменом для меня, поэтому приведенный ниже код предназначен для Azure DNS, но вы можете изменить API для работы с любым провайдером по вашему выбору, у которого есть какой-то API.

Вторая частьиз этого не должно быть никакого простоя в моем кластере высокой доступности (HA).Итак, что я сделал, это записал сертификат в базу данных, а затем прочитал его динамически при запуске моей виртуальной машины.Таким образом, в основном каждый раз, когда 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 в качестве веб-сервера, а затем динамически загружать сертификат из базы данных.Поэтому добавьте следующее к вашему 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, использующий сертификаты, чтобы они извлекали новые сертификаты.

Логика 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. Сохранение сертификатов в БД
  8. Перезапуск виртуальных машин

Действительный метод 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, а затем возможность перезапускать виртуальные машины у вашего хостинг-провайдера.Мой был весь с 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;
}

Я надеюсь, что это может помочь кому-то еще, кто оказался в том же положении, что и я.

...