Skip to content

Commit

Permalink
Added support for experimental CNAME record (#793)
Browse files Browse the repository at this point in the history
* Added support for experimental CNAME record

* Save the DNS Alias in the tag and use it when renew
  • Loading branch information
shibayan authored Jan 1, 2025
1 parent 33c2982 commit 6e06532
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 20 deletions.
4 changes: 2 additions & 2 deletions KeyVault.Acmebot/Functions/ISharedActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public interface ISharedActivity

Task<OrderDetails> Order(IReadOnlyList<string> dnsNames);

Task<string> Dns01Precondition((string, IReadOnlyList<string>) input);
Task<string> Dns01Precondition(CertificatePolicyItem certificatePolicyItem);

Task<(IReadOnlyList<AcmeChallengeResult>, int)> Dns01Authorization((string, IReadOnlyList<string>) input);
Task<(IReadOnlyList<AcmeChallengeResult>, int)> Dns01Authorization((string, string, IReadOnlyList<string>) input);

[RetryOptions("00:00:10", 12, HandlerType = typeof(ExceptionRetryStrategy<RetriableActivityException>))]
Task CheckDnsChallenge(IReadOnlyList<AcmeChallengeResult> challengeResults);
Expand Down
17 changes: 8 additions & 9 deletions KeyVault.Acmebot/Functions/SharedActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,16 @@ public async Task<OrderDetails> Order([ActivityTrigger] IReadOnlyList<string> dn
}

[FunctionName(nameof(Dns01Precondition))]
public async Task<string> Dns01Precondition([ActivityTrigger] (string, IReadOnlyList<string>) input)
public async Task<string> Dns01Precondition([ActivityTrigger] CertificatePolicyItem certificatePolicyItem)
{
var (dnsProviderName, dnsNames) = input;

// DNS zone の一覧を各 Provider から取得
var zones = await _dnsProviders.FlattenAllZonesAsync();

// DNS zone が存在するか確認
var foundZones = new HashSet<DnsZone>();
var notFoundZoneDnsNames = new List<string>();

foreach (var dnsName in dnsNames)
foreach (var dnsName in certificatePolicyItem.AliasedDnsNames)
{
var zone = zones.FindDnsZone(dnsName);

Expand Down Expand Up @@ -195,7 +193,7 @@ public async Task<string> Dns01Precondition([ActivityTrigger] (string, IReadOnly
}

// 指定された DNS Provider に属する DNS zone を優先する
var dnsProvider = foundZones.Select(x => x.DnsProvider).FirstOrDefault(x => x.Name == dnsProviderName);
var dnsProvider = foundZones.Select(x => x.DnsProvider).FirstOrDefault(x => x.Name == certificatePolicyItem.DnsProviderName);

// DNS zone の属する Provider が変わった可能性があるのでフォールバック
if (dnsProvider is null)
Expand All @@ -218,9 +216,9 @@ public async Task<string> Dns01Precondition([ActivityTrigger] (string, IReadOnly
}

[FunctionName(nameof(Dns01Authorization))]
public async Task<(IReadOnlyList<AcmeChallengeResult>, int)> Dns01Authorization([ActivityTrigger] (string, IReadOnlyList<string>) input)
public async Task<(IReadOnlyList<AcmeChallengeResult>, int)> Dns01Authorization([ActivityTrigger] (string, string, IReadOnlyList<string>) input)
{
var (dnsProviderName, authorizationUrls) = input;
var (dnsProviderName, dnsAlias, authorizationUrls) = input;

var acmeProtocolClient = await _acmeProtocolClientFactory.CreateClientAsync();

Expand Down Expand Up @@ -251,7 +249,7 @@ public async Task<string> Dns01Precondition([ActivityTrigger] (string, IReadOnly
challengeResults.Add(new AcmeChallengeResult
{
Url = challenge.Url,
DnsRecordName = challengeValidationDetails.DnsRecordName,
DnsRecordName = string.IsNullOrEmpty(dnsAlias) ? challengeValidationDetails.DnsRecordName : $"_acme-challenge.{dnsAlias}",
DnsRecordValue = challengeValidationDetails.DnsRecordValue
});
}
Expand Down Expand Up @@ -393,7 +391,8 @@ public async Task<OrderDetails> FinalizeOrder([ActivityTrigger] (CertificatePoli
{
{ "Issuer", "Acmebot" },
{ "Endpoint", _options.Endpoint.Host },
{ "DnsProvider", certificatePolicyItem.DnsProviderName }
{ "DnsProvider", certificatePolicyItem.DnsProviderName },
{ "DnsAlias", certificatePolicyItem.DnsAlias }
});

csr = certificateOperation.Properties.Csr;
Expand Down
16 changes: 8 additions & 8 deletions KeyVault.Acmebot/Functions/SharedOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ public class SharedOrchestrator
[FunctionName(nameof(IssueCertificate))]
public async Task IssueCertificate([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var certificatePolicy = context.GetInput<CertificatePolicyItem>();
var certificatePolicyItem = context.GetInput<CertificatePolicyItem>();

var activity = context.CreateActivityProxy<ISharedActivity>();

// 前提条件をチェック
certificatePolicy.DnsProviderName = await activity.Dns01Precondition((certificatePolicy.DnsProviderName, certificatePolicy.DnsNames));
certificatePolicyItem.DnsProviderName = await activity.Dns01Precondition(certificatePolicyItem);

// 新しく ACME Order を作成する
var orderDetails = await activity.Order(certificatePolicy.DnsNames);
var orderDetails = await activity.Order(certificatePolicyItem.DnsNames);

// 既に確認済みの場合は Challenge をスキップする
if (orderDetails.Payload.Status != "ready")
{
// ACME DNS-01 Challenge を実行
var (challengeResults, propagationSeconds) = await activity.Dns01Authorization((certificatePolicy.DnsProviderName, orderDetails.Payload.Authorizations));
var (challengeResults, propagationSeconds) = await activity.Dns01Authorization((certificatePolicyItem.DnsProviderName, certificatePolicyItem.DnsAlias, orderDetails.Payload.Authorizations));

// DNS Provider が指定した分だけ後続の処理を遅延させる
await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(propagationSeconds), CancellationToken.None);
Expand All @@ -44,11 +44,11 @@ public async Task IssueCertificate([OrchestrationTrigger] IDurableOrchestrationC
await activity.CheckIsReady((orderDetails, challengeResults));

// 作成した DNS レコードを削除
await activity.CleanupDnsChallenge((certificatePolicy.DnsProviderName, challengeResults));
await activity.CleanupDnsChallenge((certificatePolicyItem.DnsProviderName, challengeResults));
}

// Key Vault で CSR を作成し Finalize を実行
orderDetails = await activity.FinalizeOrder((certificatePolicy, orderDetails));
orderDetails = await activity.FinalizeOrder((certificatePolicyItem, orderDetails));

// Finalize の時点でステータスが valid の時点はスキップ
if (orderDetails.Payload.Status != "valid")
Expand All @@ -58,9 +58,9 @@ public async Task IssueCertificate([OrchestrationTrigger] IDurableOrchestrationC
}

// 証明書をダウンロードし Key Vault に保存された秘密鍵とマージ
var certificate = await activity.MergeCertificate((certificatePolicy.CertificateName, orderDetails));
var certificate = await activity.MergeCertificate((certificatePolicyItem.CertificateName, orderDetails));

// 証明書の更新が完了後に Webhook を送信する
await activity.SendCompletedEvent((certificate.Name, certificate.ExpiresOn, certificatePolicy.DnsNames));
await activity.SendCompletedEvent((certificate.Name, certificate.ExpiresOn, certificatePolicyItem.DnsNames));
}
}
7 changes: 6 additions & 1 deletion KeyVault.Acmebot/Internal/CertificateExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ public static CertificatePolicyItem ToCertificatePolicyItem(this KeyVaultCertifi
KeyType = certificate.Policy.KeyType?.ToString(),
KeySize = certificate.Policy.KeySize,
KeyCurveName = certificate.Policy.KeyCurveName?.ToString(),
ReuseKey = certificate.Policy.ReuseKey
ReuseKey = certificate.Policy.ReuseKey,
DnsAlias = certificate.Properties.Tags.TryGetDnsAlias(out var dnsAlias) ? dnsAlias : ""
};
}

private const string IssuerKey = "Issuer";
private const string EndpointKey = "Endpoint";
private const string DnsProviderKey = "DnsProvider";
private const string DnsAliasKey = "DnsAlias";

private const string IssuerValue = "Acmebot";

Expand All @@ -71,6 +73,9 @@ public static CertificatePolicyItem ToCertificatePolicyItem(this KeyVaultCertifi

private static bool TryGetDnsProvider(this IDictionary<string, string> tags, out string dnsProviderName) => tags.TryGetValue(DnsProviderKey, out dnsProviderName);

private static bool TryGetDnsAlias(this IDictionary<string, string> tags, out string dnsAlias) => tags.TryGetValue(DnsAliasKey, out dnsAlias);


private static string ToHexString(byte[] bytes)
{
ArgumentNullException.ThrowIfNull(bytes);
Expand Down
5 changes: 5 additions & 0 deletions KeyVault.Acmebot/Models/CertificatePolicyItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public class CertificatePolicyItem : IValidatableObject
[JsonProperty("reuseKey")]
public bool? ReuseKey { get; set; }

[JsonProperty("dnsAlias")]
public string DnsAlias { get; set; }

public IEnumerable<string> AliasedDnsNames => string.IsNullOrEmpty(DnsAlias) ? DnsNames : new[] { DnsAlias };

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (DnsNames is null || DnsNames.Length == 0)
Expand Down

0 comments on commit 6e06532

Please sign in to comment.