Skip to content

Commit eaff92b

Browse files
committed
Add cross-account AssumeRole support for Route 53 DNS validation.
Authenticate with IAM access keys stored in credentials, optionally assume a role in another AWS account via STS, and use the resulting session credentials to update Route 53 for ACME DNS-01 challenges. Assume-role settings use default IsCredential (stored credentials); zone ID and propagation delay remain certificate parameters.
1 parent 6901fa6 commit eaff92b

2 files changed

Lines changed: 138 additions & 14 deletions

File tree

src/Certify.Providers/DNS/AWSRoute53/DnsProviderAWSRoute53.cs

Lines changed: 135 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
using System.Linq;
44
using System.Net.Http;
55
using System.Threading.Tasks;
6+
using Amazon;
7+
using Amazon.Runtime;
68
using Amazon.Route53;
79
using Amazon.Route53.Model;
10+
using Amazon.SecurityToken;
11+
using Amazon.SecurityToken.Model;
812
using Certify.Models;
913
using Certify.Models.Config;
1014
using Certify.Models.Plugins;
@@ -41,14 +45,18 @@ public class DnsProviderAWSRoute53 : IDnsProvider
4145
{
4246
Id = "DNS01.API.Route53",
4347
Title = "Amazon Route 53 DNS API",
44-
Description = "Validates via Route 53 APIs using IAM service credentials",
48+
Description = "Validates via Route 53 APIs using IAM credentials. Optional cross-account Assume Role can be configured in stored credentials.",
4549
HelpUrl = "https://docs.certifytheweb.com/docs/dns/providers/awsroute53",
4650
PropagationDelaySeconds = 60,
4751
ProviderParameters = new List<ProviderParameter>{
4852
new ProviderParameter{ Key="accesskey",Name="Access Key", IsRequired=true, IsPassword=false },
4953
new ProviderParameter{ Key="secretaccesskey",Name="Secret Access Key", IsRequired=true, IsPassword=true },
54+
new ProviderParameter{ Key="useassumerole", Name="Assume cross-account role", IsRequired=false, Value="false", Type=OptionType.Boolean, Description="Use the access keys to assume an IAM role in another AWS account, then update Route 53 in that account." },
55+
new ProviderParameter{ Key="rolearn", Name="Role ARN", IsRequired=false, IsPassword=false, Description="Target role ARN (e.g. arn:aws:iam::123456789012:role/CertifyRoute53). Required when Assume cross-account role is enabled." },
56+
new ProviderParameter{ Key="rolesessionname", Name="Session name", IsRequired=false, IsPassword=false, Value="CertifyTheWeb", Description="Optional STS session name." },
57+
new ProviderParameter{ Key="externalid",Name="External ID (optional)", IsRequired=false, IsPassword=true, Description="Optional. Required by the target role trust policy when using cross-account Assume Role." },
58+
new ProviderParameter{ Key="zoneid",Name="DNS Zone Id", IsRequired=true, IsPassword=false, IsCredential=false, Description="Hosted zone ID from Route 53 (e.g. /hostedzone/Z1234567890)." },
5059
new ProviderParameter{ Key="propagationdelay",Name="Propagation Delay Seconds", IsRequired=false, IsPassword=false, Value="60", IsCredential=false },
51-
new ProviderParameter{ Key="zoneid",Name="DNS Zone Id", IsRequired=true, IsPassword=false, IsCredential=false },
5260
},
5361
ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS,
5462
Config = "Provider=Certify.Providers.DNS.AWSRoute53",
@@ -343,31 +351,144 @@ public async Task<List<DnsZone>> GetZones()
343351
return results;
344352
}
345353

354+
private static void ApplyProxySettings(ClientConfig clientConfig, IHttpClientProvider clientProvider, Uri serviceEndpoint)
355+
{
356+
var handler = clientProvider.CreateHandler();
357+
if (handler is HttpClientHandler httpClientHandler && httpClientHandler.Proxy != null)
358+
{
359+
var proxyUri = httpClientHandler.Proxy.GetProxy(serviceEndpoint);
360+
clientConfig.ProxyHost = proxyUri?.Host;
361+
clientConfig.ProxyPort = proxyUri?.Port ?? 0;
362+
363+
if (httpClientHandler.Proxy.Credentials != null)
364+
{
365+
clientConfig.ProxyCredentials = httpClientHandler.Proxy.Credentials;
366+
}
367+
}
368+
}
369+
370+
private static bool IsAssumeRoleEnabled(Dictionary<string, string> credentials, Dictionary<string, string> parameters)
371+
{
372+
if (TryGetBoolValue(credentials, "useassumerole", out var useFromCredentials))
373+
{
374+
return useFromCredentials;
375+
}
376+
377+
return TryGetBoolValue(parameters, "useassumerole", out var useFromParameters) && useFromParameters;
378+
}
379+
380+
private static bool TryGetBoolValue(Dictionary<string, string> source, string key, out bool value)
381+
{
382+
value = false;
383+
return source?.ContainsKey(key) == true
384+
&& bool.TryParse(source[key], out value);
385+
}
386+
387+
private static bool TryGetConfigValue(
388+
Dictionary<string, string> credentials,
389+
Dictionary<string, string> parameters,
390+
string key,
391+
out string value)
392+
{
393+
if (credentials?.TryGetValue(key, out value) == true && !string.IsNullOrWhiteSpace(value))
394+
{
395+
return true;
396+
}
397+
398+
if (parameters?.TryGetValue(key, out value) == true && !string.IsNullOrWhiteSpace(value))
399+
{
400+
return true;
401+
}
402+
403+
value = null;
404+
return false;
405+
}
406+
407+
private async Task<SessionAWSCredentials> AssumeRoleAsync(
408+
string accessKey,
409+
string secretKey,
410+
IHttpClientProvider clientProvider,
411+
string roleArn,
412+
string roleSessionName,
413+
string externalId)
414+
{
415+
var stsConfig = new AmazonSecurityTokenServiceConfig
416+
{
417+
RegionEndpoint = RegionEndpoint.USEast1
418+
};
419+
420+
ApplyProxySettings(stsConfig, clientProvider, new Uri("https://sts.amazonaws.com"));
421+
422+
using (var stsClient = new AmazonSecurityTokenServiceClient(accessKey, secretKey, stsConfig))
423+
{
424+
var request = new AssumeRoleRequest
425+
{
426+
RoleArn = roleArn,
427+
RoleSessionName = roleSessionName,
428+
DurationSeconds = 3600
429+
};
430+
431+
if (!string.IsNullOrWhiteSpace(externalId))
432+
{
433+
request.ExternalId = externalId;
434+
}
435+
436+
_log?.Debug($"Route53 :: AssumeRole : Requesting role {roleArn} with session {roleSessionName}");
437+
438+
var response = await stsClient.AssumeRoleAsync(request);
439+
var assumedCredentials = response.Credentials;
440+
441+
return new SessionAWSCredentials(
442+
assumedCredentials.AccessKeyId,
443+
assumedCredentials.SecretAccessKey,
444+
assumedCredentials.SessionToken);
445+
}
446+
}
447+
346448
public async Task<bool> InitProvider(Dictionary<string, string> credentials, Dictionary<string, string> parameters, IHttpClientProvider clientProvider, ILog log = null)
347449
{
348450
_log = log;
349451

350452
var route53Config = new AmazonRoute53Config
351453
{
352-
RegionEndpoint = Amazon.RegionEndpoint.USEast1
454+
RegionEndpoint = RegionEndpoint.USEast1
353455
};
354456

355-
// Configure proxy if needed
457+
ApplyProxySettings(route53Config, clientProvider, new Uri("https://route53.amazonaws.com"));
356458

357-
var handler = clientProvider.CreateHandler();
358-
if (handler is HttpClientHandler httpClientHandler && httpClientHandler.Proxy != null)
459+
var accessKey = credentials["accesskey"];
460+
var secretKey = credentials["secretaccesskey"];
461+
462+
if (IsAssumeRoleEnabled(credentials, parameters))
359463
{
360-
route53Config.ProxyHost = httpClientHandler.Proxy.GetProxy(new Uri("https://route53.amazonaws.com"))?.Host;
361-
route53Config.ProxyPort = httpClientHandler.Proxy.GetProxy(new Uri("https://route53.amazonaws.com"))?.Port ?? 0;
464+
if (!TryGetConfigValue(credentials, parameters, "rolearn", out var roleArn))
465+
{
466+
throw new ArgumentException("Role ARN is required when Assume Role (Cross-Account) is enabled.");
467+
}
362468

363-
// If proxy credentials are required
364-
if (httpClientHandler.Proxy.Credentials != null)
469+
var roleSessionName = "CertifyTheWeb";
470+
if (TryGetConfigValue(credentials, parameters, "rolesessionname", out var customSessionName))
365471
{
366-
route53Config.ProxyCredentials = httpClientHandler.Proxy.Credentials;
472+
roleSessionName = customSessionName;
367473
}
368-
}
369474

370-
_route53Client = new AmazonRoute53Client(credentials["accesskey"], credentials["secretaccesskey"], route53Config);
475+
TryGetConfigValue(credentials, parameters, "externalid", out var externalId);
476+
477+
var sessionCredentials = await AssumeRoleAsync(
478+
accessKey,
479+
secretKey,
480+
clientProvider,
481+
roleArn,
482+
roleSessionName,
483+
externalId);
484+
485+
_route53Client = new AmazonRoute53Client(sessionCredentials, route53Config);
486+
_log?.Information($"Route53 :: Initialized with assumed role {roleArn}");
487+
}
488+
else
489+
{
490+
_route53Client = new AmazonRoute53Client(accessKey, secretKey, route53Config);
491+
}
371492

372493
if (parameters?.ContainsKey("propagationdelay") == true)
373494
{
@@ -377,7 +498,7 @@ public async Task<bool> InitProvider(Dictionary<string, string> credentials, Dic
377498
}
378499
}
379500

380-
return await Task.FromResult(true);
501+
return true;
381502
}
382503
}
383504
}

src/Certify.Providers/DNS/AWSRoute53/Plugin.DNS.AWSRoute53.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<!-- Pin Core + service packages together; SecurityToken 4.0.7.8 requires Core >= 4.0.9.7 -->
11+
<PackageReference Include="AWSSDK.Core" Version="4.0.9.7" />
1012
<PackageReference Include="AWSSDK.Route53" Version="4.0.9.8" />
13+
<PackageReference Include="AWSSDK.SecurityToken" Version="4.0.7.8" />
1114
</ItemGroup>
1215

1316
<ItemGroup>

0 commit comments

Comments
 (0)