'CDK DnsValidatedCertificate: Can create a certificate in a linked AWS account, when the hosted zone is part of the parent account?
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": {}
}
]
}
Solution 1:[1]
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:
- Go look up the request in Amazon Certificate Manager of the linked account,
- Check (or export ) the CNAME record it is asking to add, and
- 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.
Solution 2:[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.
Solution 3:[3]
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!
Solution 4:[4]
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,
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | carpiediem |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | badfun |
