Let's Encrypt is a free, automated, and open Certificate Authority (CA) that provides SSL/TLS certificates for websites. The ACME (Automatic Certificate Management Environment) protocol is used to automate the process of obtaining and renewing SSL certificates. In this article, we will create a Windows Service that automates the process of obtaining, installing, and renewing Let's Encrypt SSL certificates for IIS websites using the ACME challenge.
Prerequisites
- Familiarity with C# and the .NET Framework
- Visual Studio installed on your system
- Administrative access to the target Windows machine running IIS
- A registered domain name with the ability to create and manage subdomains (e.g., SSLTest.wGrow.com)
Step 1: Install the required NuGet packages
- Launch Visual Studio and create a new project by selecting the "Windows Service (.NET Framework)" template.
- Name the project "IISLEService" and click "Create".
- Right-click on the project and select "Manage NuGet Packages".
- Install the following packages:
- Certes
- Microsoft.Web.Administration
Step 2: Implement the IISLEService
Open the "Service1.cs" file and rename the class to "IISLEService". Implement the required logic as follows:
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.ServiceProcess;
using System.Threading.Tasks;
using Certes;
using Certes.Acme;
using Certes.Acme.Resource;
using Microsoft.Web.Administration;
public partial class IISLEService : ServiceBase
{
private const string Domain = "SSLTest.wGrow.com";
private const string Email = "SSLTest@wGrow.com";
private const string SiteName = "Default Web Site"; // Change this to your IIS site name
private const int RenewalPeriodDays = 30;
private Timer _renewalTimer;
public IISLEService()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
// Schedule the first renewal check
_renewalTimer = new Timer(CheckCertificateRenewal, null, TimeSpan.Zero, TimeSpan.FromDays(RenewalPeriodDays));
}
protected override void OnStop()
{
_renewalTimer.Dispose();
}
private async void CheckCertificateRenewal(object state)
{
var cert = GetExistingCertificate();
if (cert == null || DateTime.UtcNow >= cert.NotAfter.AddDays(-RenewalPeriodDays))
{
await ObtainAndInstallCertificate();
}
}
private async Task ObtainAndInstallCertificate()
{
var acme = new AcmeContext(WellKnownServers.LetsEncryptV2);
var account = await acme.NewAccount(Email, true);
var order = await acme.NewOrder(new[] { Domain });
var authz = (await order.Authorizations()).First();
var httpChallenge = await authz.Http();
var keyAuthz = acme.AccountKey.ComputeKeyAuthorization(httpChallenge);
SaveChallengeFile(httpChallenge.Token, keyAuthz);
var challenge = await httpChallenge.Validate();
while (challenge.Status == ChallengeStatus.Pending)
{
await Task.Delay(2000);
challenge = await httpChallenge.Resource();
}
if (challenge.Status != ChallengeStatus.Valid)
{
throw new InvalidOperationException("ACME challenge validation failed");
}
var certChain = await order.Generate(new CsrInfo
{
CommonName = Domain,
});
InstallCertificate(certChain.Certificate.ToPem(), certChain.IssuerCertificate.ToPem());
DeleteChallengeFile(httpChallenge.Token);
}
private X509Certificate2 GetExistingCertificate()
{
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindBySubjectName, Domain, false).OfType().FirstOrDefault();
}
}
private void SaveChallengeFile(string token, string keyAuthorization)
{
// Replace this path with the appropriate path for your IIS configuration
var challengeFilePath = Path.Combine("C:\\inetpub\\wwwroot\\.well-known\\acme-challenge", token);
File.WriteAllText(challengeFilePath, keyAuthorization);
}
private void DeleteChallengeFile(string token)
{
// Replace this path with the appropriate path for your IIS configuration
var challengeFilePath = Path.Combine("C:\\inetpub\\wwwroot\\.well-known\\acme-challenge", token);
File.Delete(challengeFilePath);
}
private void InstallCertificate(string certificatePem, string issuerCertificatePem)
{
var certificate = new X509Certificate2(Convert.FromBase64String(Certes.Pkcs12Converter.Convert(certificatePem, issuerCertificatePem, "")));
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
}
using (var serverManager = new ServerManager())
{
var site = serverManager.Sites[SiteName];
var binding = site.Bindings.FirstOrDefault(b => b.Protocol == "https");
if (binding != null)
{
binding.CertificateHash = certificate.GetCertHash();
binding.CertificateStoreName = StoreName.My.ToString();
}
else
{
site.Bindings.Add($"*:{443}:{Domain}", certificate.GetCertHash(), StoreName.My.ToString(), "https");
}
serverManager.CommitChanges();
}
}
}
Step 3: Install and test the service
- Build the IISLEService project.
- Open a command prompt with administrative privileges and navigate to the directory containing the compiled IISLEService.exe.
- Install the service using the following command: `sc create IISLEService binPath= "C:\path\to\IISLEService.exe"`
- Start the service using the command: `sc start IISLEService`
The IISLEService will now automatically obtain, install, and renew the SSL certificate for SSLTest.wGrow.com.
-----
Monitoring, Troubleshooting, and Maintenance
To ensure the IISLEService operates effectively, it is crucial to monitor its performance and address any potential issues. This section will discuss best practices for monitoring, troubleshooting, and maintaining the service.
1. Logging and Monitoring
Implement logging to track the IISLEService's activity and performance. Utilize the built-in Windows Event Log to log relevant information, such as when a certificate is obtained, installed, or renewed, as well as any errors or warnings that may occur.
To add logging to the IISLEService, follow these steps:
- Add a new EventLog component to the IISLEService class and name it "eventLog".
- Set the "Log" property of the eventLog component to "Application".
- Set the "Source" property of the eventLog component to "IISLEService".
Now, you can use the `eventLog.WriteEntry()` method to log messages throughout the IISLEService. For example:
private void Log(string message, EventLogEntryType entryType = EventLogEntryType.Information)
{
eventLog.WriteEntry(message, entryType);
}
Call the `Log()` method in the appropriate places in your code to log relevant information.
2. Troubleshooting
If the IISLEService encounters issues, such as failing to obtain, install, or renew certificates, consult the logs to identify the root cause. Common issues may include:
- Incorrect domain or site configuration: Ensure that the domain name and site name in the IISLEService code match your IIS setup.
- Insufficient permissions: Verify that the IISLEService runs under an account with adequate permissions to access the IIS configuration, the certificate store, and the ACME challenge directory.
- Network issues: Check for connectivity issues between the IIS server and the Let's Encrypt infrastructure, as well as any firewalls or proxy settings that could block access.
3. Maintenance and Updates
Keep the IISLEService up-to-date with the latest security standards and best practices:
- Regularly update the .NET Framework and any third-party NuGet packages to the latest versions.
- Review and update the ACME protocol implementation to comply with any changes introduced by Let's Encrypt or the ACME standard.
- Periodically test the IISLEService's functionality to ensure it continues to operate correctly, especially after making changes to your IIS configuration or server environment.
By following these guidelines, you can effectively monitor, troubleshoot, and maintain the IISLEService, ensuring the secure and efficient management of SSL certificates for your IIS websites.