aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.59k stars 3.89k forks source link

[aws-certificatemanager] DnsValidatedCertificate support for separate hosted zones and accounts #8934

Closed podoprigo closed 1 year ago

podoprigo commented 4 years ago

Hello there!

Currently, DnsValidatedCertificate construct only allows using a single hosted zone. If the ACM certificate I'm creating needs SANs that belong to multiple hosted zones, stack creation fails.

DnsValidatedCertificate should allow skipping/waiting for SANs that belong to different hosted zones. For example, it would update correct DNS records in the target hosted zone, and display validation records for the zones it doesn't have access to, while halting stack update.

Use Case

I'm working on a project that involves separate AWS accounts per region/stage. My 'root' AWS account contains the hosted zone for the root domain (example.com), while regional accounts contain hosted zones for sub-domains (us-west-2.example.com, us-east-1.example.com).

While I can work around hosted zones creation (and the necessity to copy NS records from us-west-2.example.com to example.com), doing the same for ACM certificates is quite difficult.

For example, I need to create a certificate for us-west-2.example.com, with a SAN record for example.com.

DnsValidatedCertificate doesn't allow me to do that, since it tries to update us-west-2.example.com's DNS with 2 validation records, and the one for example.com doesn't belong there.

Certificate also doesn't allow me to do that, since it waits for DNS records to be updated : one for example.com and one for us-west-2.example.com. The problem here is that both hosted zone and certificate for us-west-2.example.com are in the same stack, so I have a bit of a chicken/egg problem.


This is a :rocket: Feature Request

jogold commented 4 years ago

Would #8552 solve this?

podoprigo commented 4 years ago

While it solves the problem with multiple hosted zones, it doesn't solve cross-account support, afaik. To solve this part of the issue, there should be an ability to assume cross-account roles and have role-per-hostedzone mapping.

konstantinj commented 4 years ago

see #4469

rrrix commented 4 years ago

@podoprigo can give some background on how you would use such a certificate? If a Route53 hosted zone is exclusive to a single account, particularly the apex domain example.com, how would multiple accounts effectively "attach" services to a single apex domain?

Are you trying to do latency based / regional DNS routing across multiple accounts? Like with an ALB or Regional API gateway in each "regional account" that only serves content for that region, but the latency based DNS routing for example.com can go to any one of those regional accounts?

podoprigo commented 4 years ago

@rrrix - you are right. Since opening this ticket, we pivoted a bit. Our setup is :

Latency based routing is setup in account A. Accounts B,C,...,Z have ACM certs for example.com and *.example.com, created using Certificate with validationMethod: ValidationMethod.DNS.

The plan for stack extension is the following:

  1. add a new region/account to stack 2.
  2. synthesize the new change for stack 2
    1. CF will halt at DNS validation step, waiting for validation data to appear in example.com DNS record
    2. copy/paste DNS validation records from stack 2 into stack 1 and synthesize stack 1
    3. wait for stack 2 synthesis to finish
  3. update stack 1 so that it now knows to redirect traffic to the new region

It works, but it requires manual intervention and copying strings across stacks. As pointed out by @konstantinj, we'll most probably need to rollout our own automation, if we need it.

Tanuel commented 4 years ago

Hi, i have a quick question: Does this issue only regard cross-account use cases? I wanted to use DnsValidatedCertificate with SANs on multiple zones for CloudFront, however it only allows to be used on a single HostedZone. The rest of the stack(s) is deployed to eu-central-1, however for CloudFront i need a certificate in us-east-1

While Certificate supports SANs, i cannot deploy it to another region, thus forcing me to deploy everything to us-east-1 since cross-region exports are also not supported and i would prefer to not use a workaround like using parameter store. we operate almost exclusively in the eu-central region so would be great to deploy our infra there

Would appreciate if DnsValidatedCertificate would support SANs

Want to follow with Code-Example:

    // approach with DnsValidatedCertificate
    const cert = new DnsValidatedCertificate(this, "MyCert", {
      hostedZone: mainZone, // <- this parameter is not required using the Certificate Class (see below)
      domainName: mainZone.zoneName,
      region: "us-east-1", // good thing here is i can set region so i can use it with CloudFront
      validation: CertificateValidation.fromDnsMultiZone(allZones),
      subjectAlternativeNames: sans,
    });

    //approach with Certificate - does not allow setting region, but doesnt require to set a specific zone
    //this works, but only in the current region
    const cert = new Certificate(this, "Certificate", {
      domainName,
      validation: CertificateValidation.fromDnsMultiZone(allZones),
      subjectAlternativeNames: sans,
    });

Error Message for reference (using DnsValidatedCertificate with multizone):

8:57:10 AM | UPDATE_FAILED | AWS::CloudFormation::CustomResource | MyStackCertifica...orResource12345 Failed to update resource. [RRSet with DNS name _asdfghjkl.example.com. is not permitted in zone www.beispiel.de.]

This writeup turned out longer than expected so im sorry for hijacking this issue TL;DR: I think this issue does not only regard cross-account use cases

aripalo commented 4 years ago

I agree with what @Tanuel said, that this issue is not just about cross-account.

It's quite common to see zone delegation even within single account, since it keeps things a bit more organized, but at the same time there are use cases to having ACM certificate that targets multiple zones.

Then again, having to deploy CloudFront certificate separately in different stack to different region (us-east-1) makes things unnecessarily complicated - compared to having a certificate construct that can deploy to us-east-1 from another region.

Having support for setting the region in certificatemanager.Certificate construct would solve these issues.

kmturley commented 3 years ago

I need this feature too. It's common to centralize Domain names and certificates in a single AWS Account. Tried using Resource Access Manager, but it doesn't allowing sharing of Domain Names and Certificates cross-account

This seems to be the best solution currently: https://stackoverflow.com/questions/58101817/cdk-dnsvalidatedcertificate-can-create-a-certificate-in-a-linked-aws-account-w

GibzonDev commented 3 years ago

Continuing discussion from #12657 @njlynch You made a suggestion that a possible workaround could be to create a Role in the "child account" and grant access to modifying the host zone in the "root account". I tried this but to no luck.

I'm posting my setup in case someone can spot an error. It would be great with a workaround for this problem.

const certRole = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::222222222222:role/TestRole', { mutable: true });
const dnsValidatedCertificate = new acm.DnsValidatedCertificate(this, 'CrossRegionCertificate', {
  domainName: 'sub.domain.com',
  hostedZone: domainZone, // Comes from "root account"
  region: this.region,
  customResourceRole: certRole,
});

TestRole has AdministratorAccess (for the sake of testing), and also an inline policy to assume a role in the root account.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::111111111111:role/ChildAccountCDKUser"
        }
    ]
}

I've added "lambda.amazonaws.com" to the trust relationship for this role (Action: sts:AssumeRole).

The ChildAccountCDKUser in the "root account" has AdministratorAccess (for the sake of testing), and also Trust relationship set to allow sts:AssumeRole for "arn:aws:iam::222222222222:root"

I'm getting this error:

9:41:45 AM | CREATE_FAILED        | AWS::CloudFormation::CustomResource       | CrossRegionCertifi...orResource/Default
Failed to create resource. User: arn:aws:sts::222222222222:assumed-role/TestRole/BackendStack-CrossRegionCertificateCertificate-YJRIF5965XXX is not authorized to access this resource

Any ideas?

skulljoi commented 3 years ago

I need this feature too add a possibility for acm.Certificate to add property region will be great! or making DnsValidatedCertificate support subjectAlternativeNames and validation from differents dns hosted zone

Stf-F commented 3 years ago

Interested in that feature too

jvlch commented 3 years ago

I've found somewhat of a workaround for my use case:

USE CASE: Desired Certificate: {region: 'us-east-1', domainName: '*.api.env.mydomain.com', SAN: ['*.mydomain.com']}

-AccountA|HostedZoneA: mydomain.com -AccountB|HostedZoneB: env.mydomain.com

This certificate will be shared amongst multiple downstream stacks in AccountB

My Stack is deployed to AccountB:us-east-2, and includes a cloudfront distribution that needs to use a us-east-1 certificate.

The dns validation entry for *.mydomain.com has already been done for AccountB, so that is already handled

I use acm.DnsValidatedCertificate to auto-create the *.api.env.mydomain.com entry in HostedZoneB, then use a custom resource to request the Desired Certificate without validation in us-east-1

There's no additional validation steps required after, as the dns validation entries are already in HostedZoneA and HostedZoneB

import cr = require('@aws-cdk/custom-resources');
import acm = require('@aws-cdk/aws-certificatemanager');
import r53 = require('@aws-cdk/aws-route53);
import logs = require('@aws-cdk/aws-logs');
import iam = require('@aws-cdk/aws-iam');

    this.publicHostedZone = new r53.PublicHostedZone(this, 'PublicHostedZone', {
      zoneName: `${env}.mydomain.com`
    });

    const certificateValidator = new acm.DnsValidatedCertificate(this, 'CertificateValidator', {
      domainName: `*.api.${env}.mydomain.com`,
      hostedZone: this.publicHostedZone,  
    })

    const certificateRequestCustomResourcePolicy = cr.AwsCustomResourcePolicy.fromStatements([new iam.PolicyStatement({
      actions: [
        'acm:ListCertificates',
        'acm:DescribeCertificates',
        'acm:RequestCertificate',
        'acm:DeleteCertificate',
      ],
      resources: ['*']
    })])

    const certificate = new cr.AwsCustomResource(this, 'Certificate', {
      policy: certificateRequestCustomResourcePolicy,
      installLatestAwsSdk: true,
      timeout: cdk.Duration.seconds(180),
      logRetention: logs.RetentionDays.ONE_WEEK,
      onCreate: {
        region: 'us-east-1',
        service: 'ACM',
        action: 'requestCertificate',
        parameters: {
          DomainName: `*.api.${env}.mydomain.com`,
          ValidationMethod: 'DNS',
          SubjectAlternativeNames: ['*.mydomain.com'],
        },
        physicalResourceId: cr.PhysicalResourceId.fromResponse('CertificateArn')
      },
      onDelete: {
        region: 'us-east-1',
        service: 'ACM',
        action: 'deleteCertificate',
        parameters: {
          CertificateArn: new cr.PhysicalResourceIdReference()
        },
      }
    })

    const certificateArn = certificate.getResponseField('CertificateArn')
markandrus commented 2 years ago

Agree with @Tanuel and @aripalo here — we need support for managing certificates (that must be created in us-east-1, for example) from other regions.

denniskribl commented 2 years ago

any updates here?

kiernan commented 2 years ago

Continuing discussion from #12657 @njlynch You made a suggestion that a possible workaround could be to create a Role in the "child account" and grant access to modifying the host zone in the "root account". I tried this but to no luck.

I don't think this would work with the current custom resource lambda for DnsValidatedCertificate, as it has no logic to change which role or credentials are used when calling the Route53 API. The calls to ACM to create the certificate, and the calls to Route53 to create the DNS records would both be made using the same role, which implies the certificate and DNS records must be in the same account. https://github.com/aws/aws-cdk/blob/0ef4bb4bf493a7e3b72b518841f676e91d014ba9/packages/%40aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/lib/index.js

For it to work, I think the lambda would need to accept another role that it can assume just when calling the Route53 API, and the construct would need to allow the lambda's role to assume it?

Or perhaps the certificate and DNS record creation could be done in separate custom resources, so they can each run with separate roles.

taylorb-syd commented 2 years ago

Identified 3 duplicates #15217, #20774 and #21040 that talk about this issue. We really need to up the priority of this issue as it is clear from these duplicates that we need to revise these issues.

Recommendations of prerequisites of how to model this with minimal disruption:

With this we have the building blocks to update certificatemanager.DnsValidatedCertificate. We can then:

At the moment I need to have two stacks, or stick to a single hosted zone, which is quite frustrating as you can imagine.

Note: As I primarily use Python the names are derived from the Python Docs in these suggestions.

github-actions[bot] commented 2 years ago

This issue has received a significant amount of attention so we are automatically upgrading its priority. A member of the community will see the re-prioritization and provide an update on the issue.

rix0rrr commented 2 years ago

Certificatemanager.DnsValidatedCertificate is the automated method of validating certificates, thus certificatemanager.DnsValidatedCertificate should be "preferred".

I actually rather think the reverse is true. DnsValidatedCertificate was written at a time when Route53 validation via CloudFormation was not possible yet, but these days it is: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-certificatemanager-certificate-domainvalidationoption.html

So DnsValidatedCertificate should probably be deprecated.

At the moment I need to have two stacks [...] which is quite frustrating as you can imagine.

I'd like to interrogate this a little. Why is having 2 stacks for resources in 2 regions frustrating ?

Conversely, adding custom resources and ways to create "out-of-region" resources for every resource where people might want them seems worse: it's less scalable development-wise, and brings more maintenance burden and chance of bugs.

Let me offer a different solution that accomplishes ~the same thing: how about we add the ability to make NestedStacks cross-region? Yes, it would be with a Custom Resource, but at least it would be scalable -- you would then be able to create cross-region anything, not just the resources we implemented explicit support for.

codeedog commented 2 years ago

I'm the filer of #15217 which has been merged into this. As a developer, I'm a big fan of reusable solutions and the cross-stack, cross-region discussion is a worthy feature that very well may cover a lot of use cases in a general fashion. I will say that the deployment requirements (and not abstract stack/region architecture) dictate that SSL Certificates must be created and live in us-east-1 while I would prefer my operational stack to live in a different region. I haven't imposed cross-region requirements on myself, AWS has. If, by your proposal, I will have to create and manage recursive stacks just to get certifcates deployed, well, that feels like punishment, not convenience. So, my request would be if you choose to build the general case, then rewrite DnsValidatedCertificate to use it.

taylorb-syd commented 2 years ago

I think based upon codeedog@'s feedback, with regards to this specific use case I think that we should probably talk to ACM about making it possible to create "global" certificates, and fix this issue once and for all. This operational constraint doesn't just apply to CDK.

I don't think this will be a trivial change as ACM uses KMS keys that are not global, meaning that certificates cannot be shared between regions. With some architectural and implementation changes I can imagine a situation where either a scope and/or custom key can be provided as part of the certificate creation process. In theory the scope would allow for global certificates that replicated using a multi-region KMS key, and the custom key would even allow for cross-account certificates.

However, this looks like it would be a way of, so until we either have a generalized way to create cross region references, or cross-region/cross-account ACM certificates, I personally do not support the deprecation of the DnsValidatedCertificate.

taylorb-syd commented 2 years ago

Certificatemanager.DnsValidatedCertificate is the automated method of validating certificates, thus certificatemanager.DnsValidatedCertificate should be "preferred".

I actually rather think the reverse is true. DnsValidatedCertificate was written at a time when Route53 validation via CloudFormation was not possible yet, but these days it is: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-certificatemanager-certificate-domainvalidationoption.html

The goal with my notes here was to minimize disruption, which to me means "don't deprecate the currently preferred method", as that is obviously going to be a breaking change for CDK consumers.

That being said, I actually agree with you, creating custom resources when they're not needed is not scalable. We should minimize the number of custom code paths we need to manage. So ultimately, deprecation of this resource is going to be preferred, and in fact seems to what has been decided for CDK 3 as per #21982.

However, there is one thing that ValidatedCertificate can't do, and the model I proposed above can, that is Cross Account Certificate Validation. This is probably an acceptable trade-off, but one we should validate if we deprecate this resource entirely.

github-actions[bot] commented 1 year ago

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so.

chrispaton commented 1 year ago

Hello! I see that this issue has been closed, but it looks like at least one of the limitations covered by this issue remains: namely that I cannot use CDK to create and validate an ACM certificate if validation requires DNS entries in a Route 53 hosted zone that is located in another account. I cannot see any other open issues covering this, so I'd like to try and resume discussion - as per the Comment visibility warning above I'm tagging previous commenters @taylorb-syd and @corymhall for visibility; shout if I should take this chat elsewhere.

I would make the case that this use case is a reasonable, and indeed desirable, thing to do when following best practices about using multiple AWS accounts in an organisation (AWS has various recommendations around this as per aws multiple account best practices - Google Search). To summarise my own use case that led me here: I have a Route 53 hosted zone for myproduct.myorg.example that is located in a "core infrastructure" AWS account. Following best practice, I then have separate AWS accounts for each stage of my service to reduce the blast radius of issues. Naturally, I want my production website to be accessible at the apex of myproduct.myorg.example rather than something like website.eu-west-1.prod.myorg.example and so I need to create an ACM TLS certificate in the EU-prod account, but validating this requires DNS entries in the core account with the top-level hosted zone, which means I cannot use either DnsValidatedCertificate nor CertificateValidation.fromDns for the reasons discussed previously.

I see that there was an effort by @johnf to solve this for DnsValidatedCertificate in #23526 but the PR was closed by @TheRealAmazonKendra as that class is now deprecated.

So a few concrete questions to try progress the conversation:

  1. Is anyone aware of any other efforts to solve for this use case?
  2. Do any of the project maintainers have a view on whether the approach on #23526 is worth rebasing onto the CertificateValidation class? This is something I could potentially have a go at progressing.
AnthonyNGarcia commented 12 months ago

Hey all, just wanted to share an approach that worked for us. Also to specifically answer @chrispaton question 1 . We basically emulated the behavior of CrossAccountZoneDelegationRecord. As a summary, the approach that construct takes is:

  1. Customer creates an IAM Role in the parent AWS account hosting the parent domain, which has permission to modify/add records to this (parent) hosted zone.
  2. Customer then provides that role when instantiating this CrossAccountZoneDelegationRecord construct in the child AWS account (hosting the subdomain hosted zone).
  3. Under the hood, this construct is leveraging an AWSCustomResource, where that role is provided to a Lambda function which makes a series of AWS SDK calls to modify the parent hosted zone.

We can do the same thing here for DNS validation. I believe the PR that was closed for this issue was taking a similar approach. Still, one could accomplish the same end result by implementing their own Lambda and AWSCustomResource to invoke it.

For example, first you'd have to create the ACM certificate and tell the CDK you'll be doing DNS validation yourself by providing undefined like this. And then the AWS custom resource can be created like this, but note where resources need to be provided with <>. Also this is a partial example since the resource listed here should ideally be broken up more for readability/maintainability:

(importing IAM, AWSCustomResource, Lambda, etc.. as needed from AWS CDK)

...

new Certificate(this, <name the certificate id here>, {
  domainName: <your domain name, the domain name to be on the certificate>
  validation: CertificateValidation.fromDns(undefined) // telling the CDK we will handle DNS validation
}

...

new AwsCustomResource(this, <name the custom resource id here>, {
  policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
  role: new Role(this, <name the role id here>, { assumedBy: new ServicePrincipal('lambda.amazonaws.com') });,
  onCreate: {
    service: 'Lambda',
    action: 'invoke',
    physicalResourceId: PhysicalResourceId.of(Date.now().toString()),
    parameters: {
      FunctionName: <lambda function>.functionName
      Payload: JSON.stringify(<request>)
    }
  },
  <ideally implement onUpdate and onDelete as well>
})

You'll also have to create the lambda function(s) yourself but there is plenty of documentation on creating a Lambda Function (including a NodeJS Lambda Function). Depending on the language you write the Lambda function in, you'll have to find the right AWS SDK (e.g., boto3 for Python).

The lambda's logic is too verbose and AWS SDK language-specific to put useful snippets here but at a high level it would accomplish something like:

  1. Expect a request which includes a cross account role ARN to assume, the certificate ARN to validate, and the hosted zone name to modify
  2. Make an API call to AWS ACM to get the domain validations from the certificate ARN (aka what it needs to insert into parent's hosted zone, the cnameName and cnameValue).
  3. Make an API call to AWS STS to assume the role by the specified role ARN, and use it for ensuing requests to R53:
  4. Make an API call to AWS R53 to get the (parent) hosted zone ID from the hosted zone name
  5. Make an API call to AWS R53 to add record for DNS validation using previously fetched cnameName and cnameValue and hosted zone ID

This also gets more complicated if the domain has additional domain names which may be owned by separate AWS accounts. In that case you'll have to get a bit fancier with how you write the Lambda function(s) to make them more versatile/dynamic, or create separate custom resources (be mindful that they have a different physical ID).

Although the above works, it is a lot of work to implement, with a lot of pitfalls. Ideally, this should be abstracted and centralized into the AWS CDK with a neat high level construct (like CrossAccountZoneDelegationRecord).

Is the above approach acceptable to the repository owners? If so, then we could have a path forward for a PR which could get merged into CDK and finally address this years-old feature request.

Just spitballing the goal's construct behavior but what if the CDK construct was extended to support something like:

new Certificate(this, 'some-cert-id', {
  domainName: 'example.com',
  validation: CertificateValidation.fromCrossAccountDns(roleArn, hostedZoneName)
  // subjectAlternativeNames: ['more.example.com'], // if validating with cross account dns, it will be assumed these domains exist in that same cross account too
  subjectAlternativeNameValidations: [altValidation1, altValidation2, etc.], // this would be optional but mutually exclusive from subjectAlternativeNames; this is simply a way to perform the same cross-account granular logic but at the subject alternative name level as well, in case they exist in different accounts (and not all in one cross account).
});

// Example of altValidation1:
const altValidation1: Certificate.SubjectAlternativeNameValidation = {
  subjectAlternativeName: 'more.example.com',
  validation: CertificateValidation.fromCrossAccountDns(anotherRoleArn, anotherHostedZoneName)
}

Alternatively, can take an approach more like fromDnsMultiZone, introducing a new fromDnsMultiZoneCrossAccount which takes domain name by key, and {hostedZoneName, roleArn} object as value:

new Certificate(this, 'some-cert-id', {
  domainName: 'example.com',
  validation: CertificateValidation.fromDnsMultiZoneCrossAccount({
    'example.com': {hostedZoneName: 'example.com', roleArn: 'abc123'},
    'another.example.com': {hostedZoneName: 'another.example.com', roleArn: 'def456'}.
  })
});

I personally prefer the second option. Assuming this might be acceptable, then I might have a go at implementing this in the AWS CDK and raising a PR. Thoughts @taylorb-syd , @TheRealAmazonKendra ?