5

I'm trying to use AWS' Cloud Development Kit to create an SSL certificate for some sub-subdomains of my website. The trouble is that I'm using AWS Organizations and the relavant resources belong to different AWS accounts. The hosted zone for my domain is part of our master account, but I'm running CDK to deploy a stack in a linked account. This means that the DnsValidatedCertificate class is able to request a new certificate (they're still visible in ACM after the stack is rolled back), but it throws an error when it attempts to create a DNS record to automatically validate the request.

Here's the error (with my account number and stack name redacted):

 5/6 | 22:44:14 | CREATE_FAILED        | AWS::CloudFormation::CustomResource | SubSubDomainsCertificate/CertificateRequestorResource/Default (SubSubDomainsCertificateCertificateRequestorResourceBC626C85) Failed to create resource. User: arn:aws:sts::123456789012:assumed-role/MyStack-SubSubDomainsCertificateCertificat-16QRI74P8POO2/MyStack-SubSubDomainsCertificateCertificat-BXZ55WHIH1XC is not authorized to access this resource
        new CustomResource (C:\repos\my-project\node_modules\@aws-cdk\aws-cloudformation\lib\custom-resource.ts:92:21)
        \_ new DnsValidatedCertificate (C:\repos\my-project\node_modules\@aws-cdk\aws-certificatemanager\lib\dns-validated-certificate.ts:81:29)
        \_ new MyStack (C:\repos\my-project\.elasticbeanstalk\api-stack.js:91:25)

And here's the relevant piece of CDK code (again, with HZ & domain redacted):

    // Executed with `cdk deploy --profile profileForLinkedAwsAccount`
    const hostedZone = route53.HostedZone.fromHostedZoneAttributes(
      this,
      'MyDomainHostedZone',
      {
        hostedZoneId: 'Z2ABC1234RYN', // in master AWS account
        zoneName: 'mydomain.com.'
      }
    );
    const certificate = new certificatemanager.DnsValidatedCertificate(
      this,
      'SubSubDomainsCertificate',
      {
        domainName: `*.demo.mydomain.com`,
        hostedZone,
        region: 'us-east-1',
        validationMethod: certificatemanager.ValidationMethod.DNS // ???
      }
    );

So, is there any way to configure CDK that will allow the DNS validation to happen automatically? Or do I need to do that as a second step, using a different profile?

EDIT: Based on Michael's suggestion, I added a role named LinkedAccountCertValidatorRole to the master AWS account. The managed policy I've attached to the role and it's trust relationship are shown below. Unfortunately, I'm still getting the same error. In addition, the Access Advisor tab indicates that the policy was never used by this role.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "route53:ChangeResourceRecordSets",
            "Resource": "arn:aws:route53:::hostedzone/Z2ABC1234RYN"
        }
    ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {}
    }
  ]
}
carpiediem
  • 1,918
  • 22
  • 41

4 Answers4

5

For the sake of completeness, I'll post the easy answer here: use the Certificate class instead of DnsValidatedCertificate. I can have CDK create a certificate request but not have it attempt to automatically validate the subdomain. This means that I have to:

  1. Go look up the request in Amazon Certificate Manager of the linked account,
  2. Check (or export ) the CNAME record it is asking to add, and
  3. Switch to the master AWS account and add the record in Route53.
// Executed with `cdk deploy --profile profileForLinkedAwsAccount`
const certificate = new certificatemanager.Certificate(this, 'SubSubDomainsCertificate', {
  domainName: `*.${SUBDOMAIN}.mydomain.com`,
  validationMethod: ValidationMethod.DNS
});

I've settled on this option, for now, but it would be nice to fully automate the process.

carpiediem
  • 1,918
  • 22
  • 41
  • I found the CDK in relation to the certificate manager very vague. I assumed `DnsValidatedCertificate` would actually try to fetch the certificate which I created manually, because I also had issues with the DNS validation when automated. However, when I use `DnsValidatedCertificate` it will still create new certificate instead of using the one that is already available. DNS Validation works because I already validated the certificate before, but now I end up with duplicates.... Ill try your approach now. – Mattijs Oct 31 '20 at 02:32
  • See my answer below; I believe this solves it the way you'd like it to be done and entirely with automation now that the API has been upgraded. – Andrew Philips Jun 14 '21 at 07:15
2

IAM can be a pain to get right. First and foremost the role you have created must have a trust relationship with a user/accounts/groups than can assume that role. I don't see that you have mentioned that in your OP. I don't know what CDK is, so I'm unable to get a clear picture of what you are doing.

Role has permissions for actions that can be performed. There is also a Trust relationship piece that defines who or what can assume that role.

Trusted relationship should have a mapping to the orgs master account like....

Create Role in Master Account with permissions attached:

My_Role_To_Assume
Assign Permissions in Master:

Trust Relationship(Master Account)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::0123456789012:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Create a group in the master account, and assign users to that group. Group permissions, should have a policy document that shows what roles and sub-account numbers the user is allowed to assume.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sts:AssumeRole"
      ],
      "Resource": [
        "arn:aws:iam::987654321098:role/My_Role_To_Assume",
        "arn:aws:iam::567890123456:role/My_Other_Role_Assume"
      ]
    }
  ]
}

Then in account you want role to be able to access. Create a role with the same name(does not have to be, but its far easier to remember what the roles are for down the line).

My_Role_To_Assume
Assign Permissions for role in sub-account:

Attach Trust Realtionship policy for sub-account role to trust master account:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::0123456789012:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

You can tweak the permission sets in each account to give much more fine-grained control/access to resources. Typically in the master account, you might not have any permissions, with the exception of IAM password, key management etc.

This approach works well, and the overall gist is you are creating trust relationships at the root level but the group policy in the master account dictates what roles can be assumed by that user/group within sub-accounts.

Since you are using the CLI, you will have to issue a aws sts call to assume the role before creating or updating resources in the sub-account. There are some scripts that handle this for you.

Example:

#! /bin/bash
#
# Dependencies:
#   yum install -y jq
#
# Setup:
#   chmod +x ./assume_cloudadmin_role.sh
#
# Execute:
#   source ./assume_cloudadmin_role.sh
#
# Description:
#   Makes assuming an AWS IAM role (+ exporting new temp keys) easier. You're users access key and secret must allow you to assume the role in the sts CLI call.

unset  AWS_SESSION_TOKEN
export AWS_ACCESS_KEY_ID=<place_your_key_here> #Master Account API Key
export AWS_SECRET_ACCESS_KEY=<place_your_secret_here>#Master Account API Secret
export AWS_REGION=us-east-1

temp_role=$(aws sts assume-role \
                    --role-arn "arn:aws:iam::0123456789012:role/My_Role_To_Assume" \
                    --role-session-name "temp_cli_role")

export AWS_ACCESS_KEY_ID=$(echo $temp_role | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $temp_role | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $temp_role | jq -r .Credentials.SessionToken)

env | grep -i AWS_

This call will set your Access key and secret + session token to perform actions on the sub-account.

Hopefully you get it working!

Michael Quale
  • 568
  • 3
  • 16
  • Sorry, it looks like you responded to my initial post, then I rewrote I a lot of it, once I discovered the root cause. Still, I think you're on the right track. I think I need to add a role to the master account that has permission to create DNS records, then use the Trust Relationships tab in IAM>Roles to allow my CLI user group in the linked account to assume that role. I'll give it a shot. – carpiediem Sep 25 '19 at 16:23
  • Got it, I see that you updated the question. for your trust relationship you can just add what I put in my edited response and as long as your master account has the correct user/role mappings it should work. AND yes, the root is correct. In the master account you would also have a role and assign users to that role. – Michael Quale Sep 25 '19 at 18:48
  • Hmmm. No luck yet. Is the `root` part of the ARN a potential issue? Since, it's an assumed role trying to connect to the master account and assume a second role? – carpiediem Sep 26 '19 at 03:58
  • 1
    No the root part is correct. Edited my answer to make it more clear. – Michael Quale Sep 26 '19 at 19:04
2

As of the date of this answer and with an update to the API, the CDK allows this type of DNS validation. I have successfully created a certificate with a subdomain and multiple alternate domains within a pre-existing hosted zone. However, I'm not working with linked accounts, so I haven't checked if that aspect works.

On DnsValidatedCertificate, the input property validation can take the result of a call to CertificateValidation.fromDnsMultiZone. This static member call takes an object whose key/value pairs represent the domains in the certificate and their IHostedZone objects. You can fetch a reference to a hosted zone by calling CDK Route53's HostedZone.fromLookup(this, id, { domainName: domain }).

Putting it all together:

import { HostedZone } from '@aws-cdk/aws-route53';
import { Certificate, CertificateValidation } from '@aws-cdk/aws-certificatemanager';

// within the stack...
const hzone = HostedZone.fromLookup(this, 'hz', { domainName: 'www.example.com' });
const cert  = new Certificate(this, 'cert', {
  domainName: site,
  validation: CertificateValidation.fromDnsMultiZone({ 'www.example.com': hzone }),
  region: 'us-east-1',
});

NOTE: You may have to make some adjustments to the above code fragment depending upon your DNS configuration. For example, www.example.com could be a CNAME or A record within the hosted zone example.com. In which case, the call to fromLookup should use example.com as the domainName instead of the www subdomain.

Also, this process creates a file cdk.context.json that retains DNS lookup context for your hosted zone reference. Here's a whole discussion thread about the need to check this file into source control.

Andrew Philips
  • 1,950
  • 18
  • 23
0

The CDK v2 has a CrossAccountZoneDelegationRecord class that allows you to setup the roles, very similar to Michael Quale's answer, but entirely in the CDK.

From the API docs:

To add a NS record to a HostedZone in different account you can do the following:

In the account containing the parent hosted zone:

const parentZone = new route53.PublicHostedZone(this, 'HostedZone', {
  zoneName: 'someexample.com',
  crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('12345678901'),
  crossAccountZoneDelegationRoleName: 'MyDelegationRole',
});

In the account containing the child zone to be delegated:

const subZone = new route53.PublicHostedZone(this, 'SubZone', {
  zoneName: 'sub.someexample.com',
});

// import the delegation role by constructing the roleArn
const delegationRoleArn = Stack.of(this).formatArn({
  region: '', // IAM is global in each partition
  service: 'iam',
  account: 'parent-account-id',
  resource: 'role',
  resourceName: 'MyDelegationRole',
});
const delegationRole = iam.Role.fromRoleArn(this, 'DelegationRole', delegationRoleArn);

// create the record
new route53.CrossAccountZoneDelegationRecord(this, 'delegate', {
  delegatedZone: subZone,
  parentHostedZoneName: 'someexample.com', // or you can use parentHostedZoneId
  delegationRole,
});
badfun
  • 145
  • 1
  • 10