Verify SES email address through CDK - amazon-web-services

I wan to verify an email address from my CDK itself so that when my stack is deployed tto some other regions this verification is automatically triggered, rather than going to AWS console and doing it manually.

You can either do that using AwsCustomResource from #aws-cdk/custom-resources and it looks similar to the example that you can find here for validating a domain: Custom Resource Examples.
Verify email using TypeScript
I'm adjusting the example here for your use case:
const verifyDomainIdentity = new AwsCustomResource(this, 'VerifyDomainIdentity', {
onCreate: {
service: 'SES',
action: 'verifyEmailIdentity',
parameters: {
EmailAddress: 'your#example.com'
},
physicalResourceId: PhysicalResourceId.of('verify-email-address')
},
policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}) // This does not work somehow with SES or maybe I did something wrong :-(
});
Unfortunately this does not work out of the box because somehow the generated policy includes an email: prefix instead of ses: and you need to provide your own policy. But there's an alternative below.
Using an existing CDK Construct with TypeScript
The other alternative is to use a CDK Construct which is already doing that for you. I recently ran into the same problem like you and I've published a CDK Construct for that: ses-verify-identities. You can then do it like this:
new VerifySesEmailAddress(this, 'SesEmailVerification', {
emailAddress: 'hello#example.org'
});
You can find the source code of the CDK construct here in case you are interested. The same is possible for verifying domains.

Related

Sharing API gateway endpoint URL across different stacks in CDK

I have following AWS CDK backed solution:
Static S3 based webpage which communicates with
API Gateway which then sends data to
AWS lambda.
The problem is that S3 page needs to be aware of API gateway endpoint URL.
Obviously this is not achievable within the same CDK stack. So I have defined two stacks:
Backend (API gateway + lambda)
Frontend (S3 based static webpage)
They are linked as dependant in CDK code:
const app = new cdk.App();
const backStack = new BackendStack(app, 'Stack-back', {...});
new FrontendStack(app, 'Stack-front', {...}).addDependency(backStack, "API URL from backend is needed");
I try to share URL as follows.
Code from backend stack definition:
const api = new apiGW.RestApi(this, 'MyAPI', {
restApiName: 'My API',
description: 'This service provides interface towards web app',
defaultCorsPreflightOptions: {
allowOrigins: apiGW.Cors.ALL_ORIGINS,
}
});
api.root.addMethod("POST", lambdaIntegration);
new CfnOutput(this, 'ApiUrlRef', {
value: api.url,
description: 'API Gateway URL',
exportName: 'ApiUrl',
});
Code from frontend stack definition:
const apiUrl = Fn.importValue('ApiUrl');
Unfortunately, instead of URL I get token (${Token[TOKEN.256]}). At the same time, I see URL is resolved in CDK generated files:
./cdk.out/Stack-back.template.json:
"ApiUrlRef": {
"Description": "API Gateway URL",
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "MyAPI7DAA778AA"
},
".execute-api.us-west-1.",
{
"Ref": "AWS::URLSuffix"
},
"/",
{
"Ref": "MyAPIDeploymentStageprodA7777A7A"
},
"/"
]
]
},
"Export": {
"Name": "ApiUrl"
}
}
},
What I'm doing wrong?
UPD:
After advice of fedonev to pass data as props, situation did not changed much. Now url looks like that:
"https://${Token[TOKEN.225]}.execute-api.us-west-1.${Token[AWS.URLSuffix.3]}/${Token[TOKEN.244]}/"
I think important part I missed (which was also pointed by
Milan Gatyas) is how I create HTML with URL of gateway.
In my frontend-stack.ts, I use template file. After template is filled, I store it in S3:
const filledTemplatePath: string = path.join(processedWebFileDir,'index.html');
const webTemplate: string = fs.readFileSync(filledTemplatePath, 'utf8')
const Handlebars = require("handlebars")
let template = Handlebars.compile(webTemplate)
const adjustedHtml: string = template({ apiGwEndpoint: apiUrl.toString() })
fs.writeFileSync(filledTemplatePath, adjustedHtml)
// bucket
const bucket: S3.Bucket = new S3.Bucket(this, "WebsiteBucket",
{
bucketName: 'frontend',
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
publicReadAccess: true,
})
new S3Deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [S3Deploy.Source.asset(processedWebFileDir)],
destinationBucket: bucket,
});
(I'm new to TS and web, please don't judge much :) )
Am I correct that S3 is populated on synth, deploy does not change anything and this is why I get tokens in html?
Will be grateful for a link or explanation so that I could understand the process better, there are so much new information to me that some parts are still quite foggy.
As #fedonev mentioned, the tokens are just placeholder values in the TypeScript application. CDK app replaces tokens with intrinsic functions when the CloudFormation template is produced.
However, your use case is different. You try to know the information inside the CDK app which is available only at synthesis time, and you can't use the intrinsic function to resolve the URL while being in CDK app to write to file.
If possible you can utilize the custom domain for the API Gateway. Then you can work with beforehand known custom domain in your static file and assign the custom domain to the API Gateway in your CDK App.
[Edit: rewrote the answer to reflect updates to the OP]
Am I correct that S3 is populated on synth, deploy does not change anything and this is why I get tokens in html?
Yes. The API URL will resolve only at deploy-time. You are trying to consume it at synth-time when you write to the template file. At synth-time, CDK represents not-yet-available values as Tokens like ${Token[TOKEN.256]}, the CDK's clever way of handling such deferred values.
What I'm doing wrong?
You need to defer the consumption of API URL until its value is resolved (= until the API is deployed). In most cases, passing constructs as props between stacks is the right approach. But not in your case: you want to inject the URL into the template file. As usual with AWS, you have many options:
Split the stacks into separate apps, deployed separately. Deploy BackendStack. Hardcode the url into FrontendStack. Quick and dirty.
Instead of S3, use Amplify front-end hosting, which can expose the URL to your template as an environment variable. Beginner friendly, has CDK support.
Add a CustomResource construct, which would be backed by a Lambda that writes the URL to the template file as part of the deploy lifecycle. This solution is elegant but not newbie-friendly.
Use a Pipeline to inject the URL variable as a build step during deploy. Another advanced approach.

Cognito attribute mapping with CDK / CloudFormation

Based on what's described here and on other pages, I created via CDK a Cognito User Pool and an Identity Pool, and, after manually mapping the custom attributes,
access is granted based on the custom attributes in the User Pool.
Now I'm trying to do everything in CDK, but I can't figure how to do the mapping of the custom attributes. The only thing I found that knows
about attribute mapping is UserPoolIdentityProvider
/ CfnUserPoolIdentityProvider,
but that is of the wrong type, and I cannot use it with
a CfnIdentityPool in cognitoIdentityProviders.
I saw some unanswered posts about the same issue (this,
or this), but
hope dies last, so I thought maybe there will be an answer this time.
I was under the impression that everything is doable via CloudFormation, but this seems mistaken, as
this post and others
suggest.
So can the attribute mapping be done with CDK, or I need to use custom resources and Lambdas (or perhaps something else) if I want to automate this?
Credits to original creator. Found this useful and solves the problem with Custom Resources.
https://github.com/aws-samples/amazon-cognito-abac-authorization-with-react-example/blob/main/lib/cognito_identity_pool_sample-stack.ts
new cognito.CfnIdentityPoolRoleAttachment(this, "defaultRoles", {
identityPoolId: identityPool.ref,
roles: {
'authenticated': authRole.attrArn
}
})
const createParameters = {
"IdentityPoolId": identityPool.ref,
"IdentityProviderName": userPool.userPoolProviderName,
"PrincipalTags": {
"department": "department"
},
"UseDefaults": false
}
const setPrincipalTagAction = {
action: "setPrincipalTagAttributeMap",
service: "CognitoIdentity",
parameters: createParameters,
physicalResourceId: customResources.PhysicalResourceId.of(identityPool.ref)
}
const { region, account } = Stack.of(this)
const identityPoolArn = `arn:aws:cognito-identity:${region}:${account}:identitypool/${identityPool.ref}`
// Creates a Custom resource (https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.custom_resources-readme.html)
// This is necessary to attach Principal Tag mappings to the Identity Pool after it has been created.
// This uses the SDK, rather than CDK code, as attaching Principal Tags through CDK is currently not supported yet
new customResources.AwsCustomResource(this, 'CustomResourcePrincipalTags', {
onCreate: setPrincipalTagAction,
onUpdate: setPrincipalTagAction,
policy: customResources.AwsCustomResourcePolicy.fromSdkCalls({
resources: [identityPoolArn],
}),
})

How can I set up my HostedZone so that it delegates to a parent DNS record in another AWS account?

Introduction
I have some TypeScript code that uses CDK to create an API Gateway and a Lambda. It works and deploys to a standard AWS URL. So far so good.
I now need to transfer the API Gateway so that it operates on a custom domain, so that it can set a cookie in a web app. This is proving far harder, and I suspect I am having difficulty because I am new to TypeScript, AWS, and CDK all at the same time. There are a number of documentation resources on the web, but most would require me to rewrite the precious little working code I have, which I am reluctant to do.
I have created a certificate manually, because that requires validation and thus it does not make sense to create it in code. Other than that I want all other resources to be created by CDK code in a Stack. In my view, it defeats the purpose of CDK if I have to configure things manually.
Problem
The below code deploys everything I need to gatekeeper.d.aws.example.com - a HostedZone, an ARecord, a LambdaRestApi and a Function (lambda). However it does not work because the NS records newly assigned to gatekeeper.d.aws.example.com do not match the ones in the parent d.aws.example.com.
I think this means that although d.aws.example.com is "known", the gateway subdomain cannot delegate to it.
Here is my working code:
// Create the lambda resource
const referrerLambda = new lambda.Function(this, 'EisReferrerLambda', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../../src/lambda')),
environment: env
});
// Set up the domain name on which the API should appear
const domainName = 'gatekeeper.d.aws.example.com';
// TODO need to fetch it with an env var? Or read from environment?
const certificateArn = 'arn:aws:acm:us-east-1:xxx:certificate/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy';
const certificate = acm.Certificate.fromCertificateArn(this, 'SslCertificate', certificateArn);
const hostedZone = new route53.HostedZone(this, 'EisReferrerHostedZone', {
zoneName: domainName
});
// Add an A record
new route53.ARecord(this, 'DnsRecord', {
zone: hostedZone,
target: route53.RecordTarget.fromAlias(new targets.ApiGateway(apiGateway)),
});
// I think I need a DomainNameOptions object
const dno : DomainNameOptions = { certificate, domainName };
// Create the APIG resource
// See https://intro-to-cdk.workshop.aws/the-workshop/4-create-apigateway.html
const apiGateway = new apigw.LambdaRestApi(this, "EisReferrerApi", {
handler: referrerLambda,
// proxy = on means that the lambda handles all requests to the APIG,
// instead of just explicit resource endpoints
proxy: false,
// deploy = on means that we get a default stage of "prod", I don't want
// that - I'm creating a custom Deployment anyway
deploy: false,
// Point to a domain name options object
domainName: dno
});
// Create an endpoint in the APIG
// https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigateway-readme.html#defining-apis
const items = apiGateway.root.addResource('gatekeeper');
items.addMethod('GET'); // GET /default/gatekeeper
// The deployment resource is just needed by the Stage system
const deployment = new apigw.Deployment(
this,
'EisReferrerDeployment',
{ api: apiGateway }
);
// Create a Stage (this affects the first component in the path
const stageName = 'default';
apiGateway.deploymentStage = new apigw.Stage(
this,
stageName,
{ deployment, stageName }
);
Question
As you can see from the code, I've found how to create an A record, but creating/modifying NS records seems harder. For a start, there does not seem to be an NSRecord class, at least based on exploring the class structure from my IDE autocomplete.
A rudimentary solution would allow me to create NS records with the fixed values that are set up elsewhere (in the AWS account that "owns" the domain). A better solution would be to read what those records are, and then use them.
Update
To see if my thinking is on the right track, I have run this deployment code, and manually modified the automatically assigned NS records in the HostedZone to match the records in the parent (in the other account). I think I have to wait for this change to seep into the DNS system, and I will update with the result.
Update 2
My manual adjustment did not work. I have therefore found a new thing to try (see "To add a NS record to a HostedZone in different account"):
// Commented out from earlier code
// const hostedZone = new route53.HostedZone(this, 'EisReferrerHostedZone', {
// zoneName: domainName
// });
// In the account containing the HostedZone
const parentZone = new route53.PublicHostedZone(this, 'HostedZone', {
zoneName: 'd.aws.example.com',
crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('12345678012')
});
// In this account
const subZone = new route53.PublicHostedZone(this, 'SubZone', {
zoneName: domainName
});
new route53.CrossAccountZoneDelegationRecord(this, 'delegate', {
delegatedZone: subZone,
parentHostedZoneId: parentZone.hostedZoneId,
delegationRole: parentZone.crossAccountDelegationRole
});
This sounds exactly what I need, but I fear the AWS documentation is out of date here - crossAccountDelegationRole is rendered in red in my IDE, and it crashes due to being undefined when cdk diff is run.
Update 3
I am assuming the property mentioned above is a typo or a reference to an outdated version of the library. I am now doing this:
new route53.CrossAccountZoneDelegationRecord(this, 'delegate', {
delegatedZone: subZone,
parentHostedZoneId: parentZone.hostedZoneId,
delegationRole: parentZone.crossAccountZoneDelegationRole
});
This feel tantalisingly close, but it crashes:
Failed to create resource. AccessDenied: User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/CustomCrossAccountZoneDelegationC-xxx is not authorized to
perform: sts:AssumeRole on resource: arn:aws:iam::yyyyyyyyyyyy:role/HostedZoneCrossAccountZoneDelegat-yyy
I wonder if I need to declare the IAM creds for the other account? I do have them.
I am not sure why permissions are needed, anyway - could it not just read the NS records in the other account and copy them to the local account? The DNS in the other account is public anyway.
I am willing to research fixing the IAM error, but this doesn't half feel like shooting in the dark. I might spend another two hours inching towards solving that sub-problem, only to find that the whole thing will fail for another reason.
Update 4
I have created a "Role" in the remote account to give "AmazonRoute53FullAccess" perms to the account that I am targetting for CDK deployment. However I still get the AccessDenied error. I wonder if I need to explicitly invoke that remote role in some fashion; how can I do that?
Was trying to do the same thing today & your post got me 90% of the way there, thanks! I ended up getting it to work with a different IAM principal (Organization) which was ok for my use case.
The crossAccountZoneDelegationPrincipal gives access to accounts hosting subzones, to access your root zone and write delegation (NS) records for the subzones.
For my use case, all the accounts resided within the same organization, so I created my root zone like this ->
const rootZone = new route53.PublicHostedZone(this, 'rootZone', {
zoneName: `root.zone`,
crossAccountZoneDelegationPrincipal: new iam.OrganizationPrincipal('o-####')
});
This sets up an IAM role with the following policy;
"Version": "2012-10-17",
"Statement": [
{
"Action": "route53:ChangeResourceRecordSets",
"Resource": "arn:aws:route53:::hostedzone/#####",
"Effect": "Allow"
}
]
}
And the following trust policy;
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-####"
}
}
}
]
}
Which effectively allows anyone with that OrgID to write records in the root zone.
In my subzones, I run with this;
const subZone = new route53.PublicHostedZone(this, 'SubZone', {
zoneName: 'sub.root.zone'
});
const delegationRole = iam.Role.fromRoleArn(this, 'delegationRole', 'arn:aws:iam::###:role/###')
new route53.CrossAccountZoneDelegationRecord(this, 'delegate', {
delegatedZone: subZone,
parentHostedZoneId: '###',
delegationRole: delegationRole
});
This ended up creating the delegation records in the root zone, for my subzone. If the organization principal doesn't fit your use case and you still need to grant multiple accounts that authority, try the composite principal https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_aws-iam.CompositePrincipal.html
Also wanted to address the concerns raised in the other answer around it being an anti-pattern & cross-account CDK being hard. This isn't really cross account CDK. This is utilizing a pattern provided by AWS (specifically spinning up a lambda to execute the provisioning of subzone records in a root zone).
Hopefully it works for you!
I have consulted two AWS experts, and they do not favour cross-account operations. One said:
This is an anti-pattern, since it requires permissions to remain even after the stack is deployed. What happens if a cross-account operation has to roll back and that permission is revoked? It would result in the app being stuck in a middle/undefined state (the local part is rolled back, but the remote part cannot be rolled back due to an access violation).
The other advised:
Cross-account CDK is hard.
It is much better to split your stacks into two or more operations, so you can run them independently. This applies nicely to "one off" operations like DNS delegation - realistically you are not going to change the zone delegation for your Stack unless you destroy it, which you are not going to do until you actually don't need it. Thus, there is no reason for the zone information to change for the lifetime of the system.
This also works well where you have an app and a database, and you want the ability to take down your app without destroying the data.
So, this is an answer in the sense that some folks will say "don't do it". However, it looks like AWS has the ability to do it, so answers in that direction are still welcome.
I used the accepted answer to create this GitHub project which implements a proof-of-concept:
https://github.com/adamcbuckley/CdkCloudfrontUsingDnsFromAnotherAccount
This sample project demonstrates how to use the AWS CDK serve static web content using a Cloudfront distribution. The Cloudfront distribution is given a domain name and an HTTPS certificate, even though the Route 53 hosted zone is owned by a different AWS Account (the parent account).

Set account recovery preference for AWS Cognito User Pool with Terraform

In the spirit of infrastructure as code, I've configured an AWS Cognito User Pool via Terraform with the helpful aws_cognito_user_pool resource.
However, I can't seem to locate the argument/config mapping for the account recovery preference under the MFA and verification section.
Without specification, it appears that this is my default selection:
(Not Recommended) Phone if available, otherwise email, and do allow a user to reset their password via phone if they are also using it for MFA.
Goal
I'd like to set this to Email only instead, as annotated by the red rectangle in the image below:
Does anyone know what Terraform argument I need to use to achieve this please? None of the options documented in the aws_cognito_user_pool resource seem to map to this.
1 year on, I can now answer my own question, due to the newly introduced setting, account_recovery_setting, of the aws_cognito_user_pool resource.
For example, to set the account recovery preference to email only, we can do the following:
resource "aws_cognito_user_pool" "mypool" {
name = "mypool"
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
}
}
This is available since v3.19.0 of the AWS provider, as part of this merged PR.
Hi Peter , I am using the CloudFormation template for creating the Cognito Configuartion.
With a little bit of modification and converting to YAML. We can have the Recovery Settings set to Email only option. Please find the below code snippet.
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
UserPoolName: "test-pool"
UsernameAttributes: [email]
AccountRecoverySetting:
RecoveryMechanisms:
- Name: "verified_email"
Priority: 1
AutoVerifiedAttributes:
- email
This seems to be working for me :)
Note: while trying to incorporate the other option for "admin_only" ,AWS geneartes the error Invalid account recovery setting parameter. Account Recovery Setting cannot use admin_only setting with any other recovery mechanisms.
Terraform doesn't support it yet.
But you could use local exec instead:
resource "null_resource" "setup_account_recovery_settings" {
triggers = {
version = "${var.version_local_exec_account_recovery_settings}"
}
provisioner "local-exec" {
command = "aws cognito-idp update-user-pool --user-pool-id ${aws_cognito_user_pool.userpool.id} --account-recovery-setting 'RecoveryMechanisms=[{Priority=1,Name=verified_email},{Priority=2,Name=verified_phone_number}]' --region ${var.region}"
}
}
But it will wipe out your whole configuration. Instead you could provide full config as a json but why to use terraform than
Following the David's idea, if you want to enable the "email only" option, you should to set
--account-recovery-setting 'RecoveryMechanisms=[{Priority=1,Name=verified_email}]'
Regards,

Using AWS CDK, how can I set the oathToken for source in code pipeline, to pull sourcecode from GitHub, without using Secret Manager service?

I am using CDK to set up code pipelines in AWS. The pipeline stage needs to download the source code from github so uses an oauth token to authenticate the request. I would like to be able to access the token from AWS Parameter Store and NOT from AWS Secret Manager when setting the value in the stage of the pipeline.
There are plenty of examples using Secret Manager to do this. However there are no examples using the Parameter Store or hardcoding the token in plain text within the CDK project.
We are using typescript with CDK 1.3.0.
I have tried storing the token in the Parameter Store. When storing as a secure String you need to additionally specify the version when retrieving the value. However I cannot then cast to a SecretValue that is required to set oauthToken property in the pipeline stage.
Get the value from the Parameter Store ..
// get the secureString
const secureString = ssm.StringParameter.fromSecureStringParameterAttributes(construct,'MySecretParameter', {
parameterName: 'my-secure-parameter-name',
version: 1,
});
I need to cast the secretString to a CDK.SecretValue to then use it to set the oauthToken. I cannot see how to do this.
const sourceAction = new codepipelineactions.GitHubSourceAction({
actionName: 'Source',
owner: owner,
repo: repository,
oauthToken: githubOAuthAccessToken,
output: sourceOutput,
branch: branch,
trigger: codepipelineactions.GitHubTrigger.WEBHOOK,
});
The CDK documentation says that is is advisable to store tokens in Secret Manager.
"It is recommended to use a Secret Manager SecretString to obtain the token"
It does not say that tokens cannot be retrieved from other sources and used. I would be grateful if the situation could be clarified and if anyone stores tokens outside Secrets Manager and is still able to use them to set the Token in the source stage of a pipeline.
You can use cdk.SecretValue.ssmSecure or cdk.SecretValue.plainText:
oauthToken: cdk.SecretValue.ssmSecure('param-name', 'version');
// OR
oauthToken: cdk.SecretValue.plainText('oauth-token-here');
From the doc for plainText:
Do not use this method for any secrets that you care about. The only reasonable use case for using this method is when you are testing.
The previous answer by #jogold does partially work. However, at the time of this writing SecretValue.ssmSecure is not supported by Cloudformation and you will get an error such as: FAILED, SSM Secure reference is not supported in: .
There is an open issue on the CDK roadmap: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/227. The plaintext option is not truly viable as the secret will be exposed in CFN template.