33using System . Linq ;
44using System . Net . Http ;
55using System . Threading . Tasks ;
6+ using Amazon ;
7+ using Amazon . Runtime ;
68using Amazon . Route53 ;
79using Amazon . Route53 . Model ;
10+ using Amazon . SecurityToken ;
11+ using Amazon . SecurityToken . Model ;
812using Certify . Models ;
913using Certify . Models . Config ;
1014using 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}
0 commit comments