Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions AcmeCaPlugin/AcmeCaPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,20 @@ public async Task<EnrollmentResult> Enroll(
// Store pending order immediately
var accountId = accountDetails.Kid.Split('/').Last();

// Process challenges
await ProcessAuthorizations(acmeClient, order, config);
// Process challenges, then finalize. Challenge TXT records are removed afterwards
// whether or not the order succeeded.
var createdRecords = new List<string>();
try
{
await ProcessAuthorizations(acmeClient, order, config, createdRecords);

// Finalize with original CSR bytes
order = await acmeClient.FinalizeOrderAsync(order, csrBytes);
// Finalize with original CSR bytes
order = await acmeClient.FinalizeOrderAsync(order, csrBytes);
}
finally
{
await CleanupDnsRecordsAsync(config, createdRecords);
}

// If order is valid immediately, download cert
if (order.Payload?.Status == "valid" && !string.IsNullOrEmpty(order.Payload.Certificate))
Expand Down Expand Up @@ -492,7 +501,7 @@ private List<Identifier> ExtractDomainsFromCsr(byte[] csrBytes)
/// <summary>
/// Processes ACME authorizations with DNS verification before challenge submission
/// </summary>
private async Task ProcessAuthorizations(AcmeClient acmeClient, OrderDetails order, AcmeClientConfig config)
private async Task ProcessAuthorizations(AcmeClient acmeClient, OrderDetails order, AcmeClientConfig config, List<string> createdRecords)
{
if (order?.Payload is not Order payload || payload.Authorizations == null)
{
Expand Down Expand Up @@ -527,6 +536,7 @@ private async Task ProcessAuthorizations(AcmeClient acmeClient, OrderDetails ord
// Create DNS record (will throw exception with details if it fails)
var dnsProvider = DnsProviderFactory.Create(config, _logger);
await dnsProvider.CreateRecordAsync(validation.DnsRecordName, validation.DnsRecordValue);
createdRecords.Add(validation.DnsRecordName);

_logger.LogInformation("Created DNS record {RecordName} for domain {Domain}",
validation.DnsRecordName, authz.Identifier.Value);
Expand Down Expand Up @@ -573,6 +583,44 @@ private async Task ProcessAuthorizations(AcmeClient acmeClient, OrderDetails ord
}
}

/// <summary>
/// Removes DNS-01 challenge TXT records. Best effort: failures are logged and never
/// affect the enrollment result.
/// </summary>
private async Task CleanupDnsRecordsAsync(AcmeClientConfig config, IEnumerable<string> recordNames)
{
// Wildcard and base-domain authorizations share one record name, so delete each name once.
var names = recordNames.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
if (names.Count == 0)
return;

IDnsProvider dnsProvider;
try
{
dnsProvider = DnsProviderFactory.Create(config, _logger);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not create DNS provider for challenge record cleanup");
return;
}

foreach (var name in names)
{
try
{
if (await dnsProvider.DeleteRecordAsync(name))
_logger.LogInformation("Removed DNS record {RecordName}", name);
else
_logger.LogWarning("DNS record {RecordName} was not removed; it may need manual cleanup", name);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to remove DNS record {RecordName}; it may need manual cleanup", name);
}
}
}

/// <summary>
/// Encodes binary data to PEM format with specified label
/// </summary>
Expand Down
30 changes: 21 additions & 9 deletions AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,40 @@
var (zoneName, zoneId) = await FindZoneForRecordAsync(recordName);
if (zoneId == null || zoneName == null) return false;

var relativeName = GetRelativeRecordName(recordName, zoneName);
// Cloudflare's name filter matches the fully-qualified record name, not the zone-relative one
var fullName = Uri.EscapeDataString(recordName.TrimEnd('.'));

var recordsResp = await _httpClient.GetAsync($"zones/{zoneId}/dns_records?type=TXT&name={relativeName}");
var recordsResp = await _httpClient.GetAsync($"zones/{zoneId}/dns_records?type=TXT&name={fullName}");
if (!recordsResp.IsSuccessStatusCode) return false;

var json = await recordsResp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(json);
using var doc = JsonDocument.Parse(json);

var resultArray = doc.RootElement.GetProperty("result");
if (resultArray.GetArrayLength() == 0) return false;
var recordId = resultArray[0].GetProperty("id").GetString();

if (recordId == null) return false;
// A wildcard and its base domain publish two TXT values under the same name; remove all of them
var allDeleted = true;
foreach (var record in resultArray.EnumerateArray())
{
var recordId = record.GetProperty("id").GetString();
if (recordId == null)
{
allDeleted = false;
continue;
}

var deleteResp = await _httpClient.DeleteAsync($"zones/{zoneId}/dns_records/{recordId}");
var result = await deleteResp.Content.ReadAsStringAsync();

var deleteResp = await _httpClient.DeleteAsync($"zones/{zoneId}/dns_records/{recordId}");
var result = await deleteResp.Content.ReadAsStringAsync();
Console.WriteLine($"Delete TXT: {deleteResp.StatusCode} - {result}");
allDeleted &= deleteResp.IsSuccessStatusCode;
}

Console.WriteLine($"Delete TXT: {deleteResp.StatusCode} - {result}");
return deleteResp.IsSuccessStatusCode;
return allDeleted;
}

private async Task<string?> GetZoneIdAsync(string zoneName)

Check warning on line 95 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 95 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 95 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 95 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
var response = await _httpClient.GetAsync($"zones?name={zoneName}");
if (!response.IsSuccessStatusCode) return null;
Expand All @@ -92,7 +104,7 @@
return resultArray[0].GetProperty("id").GetString();
}

private async Task<(string? zoneName, string? zoneId)> FindZoneForRecordAsync(string recordName)

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 107 in AcmeCaPlugin/Clients/DNS/CloudflareDnsProvider.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if (string.IsNullOrWhiteSpace(recordName))
return (null, null);
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v1.3.2
* Fixed DNS-01 challenge TXT records not being removed after certificate issuance
* Fixed Cloudflare DNS record deletion lookup and removal of all matching TXT records

# v1.3.0
* Containerization Changes for SaaS Environment
* Fixed URL CaId Length issue
Expand Down
Loading