AWS CDK destroy fails to delete secret - amazon-web-services

I have a CDK script that creates an S3 bucket, VPC, and an RDS instance. Deploy is working, but the destroy fails with an error that my user is not authorized to secretsmanager:DeleteSecret.
I used the IAM policy testing tool to check and it passes. I am able to delete the secret via the UI. The CDK destroy command continues to fail though. Any thoughts?
CDK script:
class AcmeCdkStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// create a general purpose bucket for use with the app
new s3.Bucket(this, 'app-bucket', {
versioned: true
});
// create a vpc for our application
const vpc = new ec2.Vpc(this, 'app-vpc, {
cidr: "10.0.0.0/16",
});
// create a database instance
const db = new rds.DatabaseInstance(this, `app-db`, {
engine: rds.DatabaseInstanceEngine.POSTGRES,
instanceClass: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
vpc,
masterUsername: `dbadmin`,
deleteAutomatedBackups: false,
deletionProtection: false,
// https://github.com/aws/aws-cdk/issues/4036
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}
const app = new cdk.App();
new AcmeCdkStack(app, 'app-stack;);
Error:
User: arn:aws:iam::0000000000:user/user#acme.com is not authorized to perform: secretsmanager:DeleteSecret on resource: arn:aws:secretsmanager:us-east-1:0000000000:secret:appdbdemoSecret0261-mjgIXOsp5rLL-HxFng1 (Service: AWSSecretsManager; Status Code: 400; Error Code: AccessDeniedException; Request ID: 000000000)

Based on the comments, the problem was that the CDK was using different credentials than expected. The solution was to use correct AWS_PROFILE.

Related

Fargate Service Cannot Write to DynamoDB Table (CDK)

I'm working through setting up a new infrastructure with the AWS CDK and I'm trying to get a TypeScript app running in Fargate to be able to read/write from/to a DynamoDB table, but am hitting IAM issues.
I have both my Fargate service and my DynamoDB Table defined, and both are running as they should be in AWS, but whenever I attempt to write to the table from my app, I am getting an access denied error.
I've tried the solutions defined in this post, as well as the ones it links to, but nothing seems to be allowing my container to write to the table. I've tried everything from setting table.grantReadWriteData(fargateService.taskDefinition.taskRole) to the more complex solutions described in the linked articles of defining my own IAM policies and setting the effects and actions, but I always just get the same access denied error when attempting to do a putItem:
AccessDeniedException: User: {fargate-service-arn} is not authorized to perform: dynamodb:PutItem on resource: {dynamodb-table} because no identity-based policy allows the dynamodb:PutItem action
Am I missing something, or a crucial step to make this possible?
Any help is greatly appreciated.
Thanks!
Edit (2022-09-19):
Here is the boiled down code for how I'm defining my Vpc, Cluster, Container Image, FargateService, and Table.
export class FooCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new Vpc(this, 'FooVpc', {
maxAzs: 2,
natGateways: 1
});
const cluster = new Cluster(this, 'FooCluster', { vpc });
const containerImage = ContainerImage.fromAsset(
path.join(__dirname, '/../app'),
{
platform: Platform.LINUX_AMD64 // I'm on an M1 Mac and images weren't working appropriately without this
}
);
const fargateService = new ApplicationLoadBalancedFargateService(
this,
'FooFargateService',
{
assignPublicIp: true,
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 1,
taskImageOptions: {
containerPort: PORT,
image: containerImage
}
}
);
fargateService.targetGroup.configureHealthCheck({ path: '/health' });
const serverTable = new Table(this, 'FooTable', {
billingMode: BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: 'id', type: AttributeType.STRING },
pointInTimeRecovery: true
});
serverTable.grantReadWriteData(fargateService.taskDefinition.taskRole);
}
}
Apparently either order in which the resources are defined matters, or the inclusion of a property from the table in the Fargate service is what did the trick. I moved the table definition up above the Fargate service and included an environment variable holding the table name and it's working as intended now.
export class FooCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new Vpc(this, 'FooVpc', {
maxAzs: 2,
natGateways: 1
});
const cluster = new Cluster(this, 'FooCluster', { vpc });
const containerImage = ContainerImage.fromAsset(
path.join(__dirname, '/../app'),
{
platform: Platform.LINUX_AMD64 // I'm on an M1 Mac and images weren't working appropriately without this
}
);
const serverTable = new Table(this, 'FooTable', {
billingMode: BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: 'id', type: AttributeType.STRING },
pointInTimeRecovery: true
});
const fargateService = new ApplicationLoadBalancedFargateService(
this,
'FooFargateService',
{
assignPublicIp: true,
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 1,
taskImageOptions: {
containerPort: PORT,
image: containerImage,
environment: {
SERVER_TABLE_NAME: serverTable.tableName
}
}
}
);
fargateService.targetGroup.configureHealthCheck({ path: '/health' });
serverTable.grantReadWriteData(fargateService.taskDefinition.taskRole);
}
}
Hopefully this helps someone in the future who may come across the same issue.

How do I make my CloudFormation / CodePipeline update a domain name to point to an S3 bucket when using CDK?

I'm using CDK to deploy a CodePipeline that builds and deploys a React application to S3. All of that is working, but I want the deployment to update a domain name to point that S3 bucket.
I already have the Zone defined in Route53 but it is defined by a different cloud formation stack because there are a lot of details that are not relevant for this app (MX, TXT, etc). What's the right way for my Pipeline/Stacks to set those domain names?
I could think of two solutions:
Delegate the domain to another zone, so zone example.com delegates staging.example.com.
Have my pipeline inject records into the existing zone.
I didn't try the delegation zone method. I was slightly concerned about manually maintaining the generated nameservers from staging.example.com into my CloudFormation for zone example.com.
I did try injecting the records into the existing zone, but I run into some issues. I'm open to either solving these issues or doing this whichever way is correct.
In my stack (full pipeline at the bottom) I first define and deploy to the bucket:
const bucket = new s3.Bucket(this, "Bucket", {...})
new s3d.BucketDeployment(this, "WebsiteDeployment", {
sources: [...],
destinationBucket: bucket
})
then I tried to retrieve the zone and add the CNAME to it:
const dnsZone = route53.HostedZone.fromLookup(this, "DNS zone", {domainName: "example.com"})
new route53.CnameRecord(this, "cname", {
zone: dnsZone,
recordName: "staging",
domainName: bucket.bucketWebsiteDomainName
})
This fails due to lack of permissions to the zone, which is reasonable:
[Container] 2022/01/30 11:35:17 Running command npx cdk synth
current credentials could not be used to assume 'arn:aws:iam::...:role/cdk-hnb659fds-lookup-role-...-us-east-1', but are for the right account. Proceeding anyway.
[Error at /Pipeline/Staging] User: arn:aws:sts::...:assumed-role/Pipeline-PipelineBuildSynthCdkBuildProje-1H5AV7C28FZ3S/AWSCodeBuild-ada5ef88-cc82-4309-9acf-11bcf0bae878 is not authorized to perform: route53:ListHostedZonesByName because no identity-based policy allows the route53:ListHostedZonesByName action
Found errors
To try to solve that, I added rolePolicyStatements to my CodeBuildStep
rolePolicyStatements: [
new iam.PolicyStatement({
actions: ["route53:ListHostedZonesByName"],
resources: ["*"],
effect: iam.Effect.ALLOW
})
]
which might make more sense in the context of the whole file (at the bottom of this question). That had no effect. I'm not sure if the policy statement is wrong or I'm adding it to the wrong role.
After adding that rolePolicyStatements, I run cdk deploy which showed me this output:
> cdk deploy
✨ Synthesis time: 33.43s
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬──────────┬────────┬───────────────────────────────┬───────────────────────────────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼──────────┼────────┼───────────────────────────────┼───────────────────────────────────────────────────────────┼───────────┤
│ + │ * │ Allow │ route53:ListHostedZonesByName │ AWS:${Pipeline/Pipeline/Build/Synth/CdkBuildProject/Role} │ │
└───┴──────────┴────────┴───────────────────────────────┴───────────────────────────────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
After deployment finishes, there's a role that I can see in the AWS console that has:
{
"Action": "route53:ListHostedZonesByName",
"Resource": "*",
"Effect": "Allow"
}
The ARN of the role is arn:aws:iam::...:role/Pipeline-PipelineBuildSynthCdkBuildProje-1H5AV7C28FZ3S. I'm not 100% if the permissions are being granted to the right thing.
This is my whole CDK pipeline:
import * as path from "path";
import {Construct} from "constructs"
import * as pipelines from "aws-cdk-lib/pipelines"
import * as cdk from "aws-cdk-lib"
import * as s3 from "aws-cdk-lib/aws-s3"
import * as s3d from "aws-cdk-lib/aws-s3-deployment"
import * as iam from "aws-cdk-lib/aws-iam"
import * as route53 from "aws-cdk-lib/aws-route53";
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props)
const bucket = new s3.Bucket(this, "Bucket", {
websiteIndexDocument: "index.html",
websiteErrorDocument: "error.html",
publicReadAccess: true,
})
const dnsZone = route53.HostedZone.fromLookup(this, "DNS zone", {domainName: "example.com"})
new route53.CnameRecord(this, "cname", {
zone: dnsZone,
recordName: "staging",
domainName: bucket.bucketWebsiteDomainName
})
new s3d.BucketDeployment(this, "WebsiteDeployment", {
sources: [s3d.Source.asset(path.join(process.cwd(), "../build"))],
destinationBucket: bucket
})
}
}
export class DeployStage extends cdk.Stage {
public readonly mainStack: MainStack
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props)
this.mainStack = new MainStack(this, "MainStack", {env: props?.env})
}
}
export interface PipelineStackProps extends cdk.StackProps {
readonly githubRepo: string
readonly repoBranch: string
readonly repoConnectionArn: string
}
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: PipelineStackProps) {
super(scope, id, props)
const pipeline = new pipelines.CodePipeline(this, id, {
pipelineName: id,
synth: new pipelines.CodeBuildStep("Synth", {
input: pipelines.CodePipelineSource.connection(props.githubRepo, props.repoBranch, {connectionArn: props.repoConnectionArn}),
installCommands: [
"npm install -g aws-cdk"
],
commands: [
// First build the React app.
"npm ci",
"npm run build",
// Now build the CF stack.
"cd infra",
"npm ci",
"npx cdk synth"
],
primaryOutputDirectory: "infra/cdk.out",
rolePolicyStatements: [
new iam.PolicyStatement({
actions: ["route53:ListHostedZonesByName"],
resources: ["*"],
effect: iam.Effect.ALLOW
})
]
},
),
})
const deploy = new DeployStage(this, "Staging", {env: props?.env})
const deployStage = pipeline.addStage(deploy)
}
}
You cannot depend on CDK pipeline to fix itself if the synth stage is failing, since the Pipeline CloudFormation Stack is changed in the SelfMutate stage which uses the output of the synth stage. You will need to do one of the following options to fix your pipeline:
Run cdk synth and cdk deploy PipelineStack locally (or anywhere outside the pipeline, where you have the required AWS IAM permissions). Edit: You will need to temporarily set selfMutatation to false for this to work (Reference)
Temporarily remove route53.HostedZone.fromLookup and route53.CnameRecord from your MainStack while still keeping the rolePolicyStatements change. Commit and push your code, let CodePipeline run once, making sure that the Pipeline self mutates and the IAM role has the required additional permissions. Add back the route53 constructs, commit, push again and check whether your code works with the new changes.

route53 returns forbidden for custom domain with API Gateway

I'm using AWS CDK to create an APIGateway. I want to attach a custom domain to my api so I can use api.findtechjobs.io. In the console, I can see I have a custom domain attached, however I always get a 403 response when using my custom domain.
Below is the following AWS CDK Stack I am using to create my API Gateway attached with a single lambda function.
AWS CDK deploys well, however, when I attempt to make a POST request to https://api.findtechjobs.io/search AWS returns a 403 Forbidden response. I don't have a VPC, WAF, or an API key for this endpoint.
I am very uncertain why my custom domain is returning a 403 response. I have been reading a lot of documentation, and used answers from other questions and I still can't figure out what I am doing wrong.
How can I associate api.findtechjobs.io to my API Gateway well using AWS CDK?
export class HostingStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
super(scope, id, props)
const zonefindtechjobsio = route53.HostedZone.fromLookup(this, 'findtechjobs.io', {
domainName: 'findtechjobs.io'
});
const certificate = new acm.Certificate(this, 'APICertificate', {
domainName: 'findtechjobs.io',
subjectAlternativeNames: ['api.findtechjobs.io'],
validation: acm.CertificateValidation.fromDns(zonefindtechjobsio),
});
const api = this.buildAPI(certificate)
new route53.ARecord( this, "AliasRecord api.findtechjobs.io", {
zone: zonefindtechjobsio,
recordName: `api`,
target: route53.RecordTarget.fromAlias(new route53targets.ApiGateway(api)),
});
}
private buildAPI(certificate: acm.Certificate) {
// API
const api = new apigateway.RestApi(this, "techjobapi", {
domainName: {
domainName: 'findtechjobs.io',
certificate: certificate
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS, // TODO limit this when you go to prod
},
deploy: true,
deployOptions: {
stageName: 'dev',
},
endpointTypes: [apigateway.EndpointType.REGIONAL]
});
const searchResource = api.root.addResource("search", {
defaultMethodOptions: {
operationName: "Search",
},
});
searchResource.addMethod(
"POST",
new apigateway.LambdaIntegration(new lambda.Function(this, "SearchLambda", {
runtime: lambda.Runtime.GO_1_X,
handler: "main",
code: lambda.Code.fromAsset(path.resolve("..", "search", "main.zip")),
environment: {
DB_NAME: "...",
DB_CONNECTION:"...",
},
})),
{
operationName: "search",
}
);
return api;
}
}
Same problem. After some struggle. I found out that the problem may lay in the DNS. Cause my domain was transferred from another registrar. The name server is not changed. After I change them to AWS dns it worked. But I can't 100% sure.
And I found out that the default API gateway domain(d-lb4byzxxx.execute-api.ap-east-1.amazonaws.com ) is always in 403 forbidden state.

How to provide a custom domain name for a Lambda-based APIGateway in CDK?

For the purposes of this question, assume that I already have an example.org Hosted Zone in Route53 (my actual zone is, of course, different)
With the following CDK app:
export class MyExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const backend = new Function(this, 'backendLambda', {
code: new AssetCode("lambda/"),
handler: "index.handler",
runtime: Runtime.PYTHON_3_8
});
apiDomainName = 'api.test.example.org'
const api = new LambdaRestApi(this, 'api', {
handler: backend,
proxy: true,
deploy: true,
domainName: {
domainName: apiDomainName,
certificate: new Certificate(this, 'apiCertificate', {
domainName: apiDomainName
})
}
});
}
}
, when I run cdk deploy, part of the output reads:
Outputs:
MyExampleStack.apiEndpoint0F54D2EA = https://<alphanumericId>.execute-api.us-east-1.amazonaws.com/prod/
, and, indeed, when I curl that url, I see the response I would expect from my Lambda code. I would expect curling api.test.example.org to give the same result - however, instead it gives curl: (6) Could not resolve host: api.test.example.org.
Based on this documentation, I tried:
export class MyExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const rootDomain = 'example.org'
const zone = HostedZone.fromLookup(this, 'baseZone', {
domainName: rootDomain
});
const backend = new Function(...);
const api = new LambdaRestApi(...);
new ARecord(this, 'apiDNS', {
zone: zone,
recordName: 'api.test',
target: RecordTarget.fromAlias(new ApiGateway(api))
});
}
}
which did give a Route53 entry:
$ aws route53 list-hosted-zones
{
"HostedZones": [
{
"Id": "/hostedzone/ZO3B2N6W70PDD",
"Name": "example.org.",
"CallerReference": "598D71AB-4A98-EC5A-A170-D51CB243A2EA",
"Config": {
"PrivateZone": false
},
"ResourceRecordSetCount": 8
}
]
}
$ aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/ZO3B2N6W70PDD --query 'ResourceRecordSets[?Name==`api.test.example.org.`]'
[
{
"Name": "api.test.example.org.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z1UJRXOUMOOFQ9",
"DNSName": "<alphanumericId2>.execute-api.us-east-1.amazonaws.com.",
"EvaluateTargetHealth": false
}
}
]
But this still doesn't work:
curl api.test.example.org still gives "Could not resolve host"
curl <alphanumericId2>.execute-api.us-east-1.amazonaws.com gives curl: (7) Failed to connect to <alphanumericId2>.execute-api.us-east-1.amazonaws.com port 80: Connection refused
curl https://<alphanumericId2>..execute-api.us-east-1.amazonaws.com gives {"message":"Forbidden"}
curl https://<alphanumericId>.[...] (i.e. the output from cdk deploy) still gives the expected response from the Lambda
How can I define a custom name in Route53 to route to my Lambda-backed APIGateway API?
Overall code LambdaRestApi with Route53 A Record, will create
Custom domain pointing to a particular stage prod i.e api.test.example.org domain to stage `prod'(example)
Route 53 A record for api.test.example.org pointing to Api Gateway hosted zone.
These are two combinations that will work
https://api.test.example.org will work pointing directly to stage prod.
CDK Output https://abcdef1234.execute-api.us-east-1.amazonaws.com/prod/ will work as stage is appended to it.
These are few combinations that will not work
Two other tests you did with http://
With no protocol, defaults to http, will not work, as we api gateway by default gives TLS 1.0 (ssl-https) and no http listener.
One other attempt you did with https:// without a stage name at the end, will return 403 forbidden, as stage name is missing.
Here is full CDK code.
import * as cdk from "#aws-cdk/core";
import * as apigw from "#aws-cdk/aws-apigateway";
import * as acm from "#aws-cdk/aws-certificatemanager";
import * as route53 from "#aws-cdk/aws-route53";
import * as route53Targets from "#aws-cdk/aws-route53-targets";
import * as lambda from "#aws-cdk/aws-lambda";
export class HelloCdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.buildLambdaApiGateway();
}
buildLambdaApiGateway() {
const rootDomain = "example.org";
const zone = route53.HostedZone.fromLookup(this, "baseZone", {
domainName: rootDomain,
});
const backend = new lambda.Function(this, "MyLayeredLambda", {
code: new lambda.InlineCode("foo"),
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_10_X,
});
const restApi = new apigw.LambdaRestApi(this, "myapi", {
handler: backend,
domainName: {
domainName: `api-test.${rootDomain}`,
certificate: acm.Certificate.fromCertificateArn(
this,
"my-cert",
"arn:aws:acm:us-east-1:111112222333:certificate/abcd6805-1234-4159-ac38-761acdc700ef"
),
endpointType: apigw.EndpointType.REGIONAL,
},
});
new route53.ARecord(this, "apiDNS", {
zone: zone,
recordName: "api-test",
target: route53.RecordTarget.fromAlias(
new route53Targets.ApiGateway(restApi)
),
});
}
}

How do I generate an IAM service specific credential using aws cdk?

I'm trying to figure out how to generate Service Specific Credentials for an IAM User with the AWS CDK.
I can see how to achieve this from:
Admin Console: IAM > Users > Security credentials:
HTTPS Git credentials for AWS CodeCommit, and
Credentials for Amazon Managed Apache Cassandra Service (MCS)
API: CreateServiceSpecificCredential
CLI: create-service-specific-credential
However I can't see how to achieve this with the AWS CDK (or from Cloud Formation for that matter).
If this is not currently supported from the CDK then what would be the recommended approach?
Building on what #JeffreyGoines replied above, a Construct calling CreateServiceSpecificCredential:
export class CodeCommitGitCredentialsProps {
userName: string
}
export class CodeCommitGitCredentials extends Construct {
readonly serviceSpecificCredentialId: string;
readonly serviceName: string;
readonly serviceUserName: string;
readonly servicePassword: string;
readonly status: string;
constructor(scope: Construct, id: string, props: CodeCommitGitCredentialsProps) {
super(scope, id);
// Create the Git Credentials required
const gitCredResp = new AwsCustomResource(this, "gitCredentials", {
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/IAM.html#createServiceSpecificCredential-property
onCreate: {
service: "IAM",
action: "createServiceSpecificCredential",
parameters: {
ServiceName: "codecommit.amazonaws.com",
UserName: props.userName
},
physicalResourceId: PhysicalResourceId.fromResponse("ServiceSpecificCredential.ServiceSpecificCredentialId")
},
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/IAM.html#deleteServiceSpecificCredential-property
onDelete: {
service: "IAM",
action: "deleteServiceSpecificCredential",
parameters: {
ServiceSpecificCredentialId: new PhysicalResourceIdReference(),
UserName: props.userName
}
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
this.serviceSpecificCredentialId = gitCredResp.getResponseField("ServiceSpecificCredential.ServiceSpecificCredentialId");
this.serviceName = gitCredResp.getResponseField("ServiceSpecificCredential.ServiceName");
this.serviceUserName = gitCredResp.getResponseField("ServiceSpecificCredential.ServiceUserName");
this.servicePassword = gitCredResp.getResponseField("ServiceSpecificCredential.ServicePassword");
this.status = gitCredResp.getResponseField("ServiceSpecificCredential.Status");
}
}
And a usage example:
// User created for Git Push/Pull
this.user = new User(this, `codeCommitGitMirrorUser`, {
userName: `${props.repository.repositoryName}-GitMirrorUser`
});
props.repository.grantPullPush(this.user);
this.gitCredentials = new CodeCommitGitCredentials(this, "codeCommitGitCredentials", {
userName: this.user.userName
});