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