Getting the security right in applications is tricky. Most developers did not undergo professional security training and are not adept in such topics. At least I know I'm not.
Luckily, multiple tools can help us achieve a relatively good security posture. Snyk, CodeQL and GitGuardian are good examples.
In some cases, even a deployment framework can be helpful. This blog post will expand on this topic, looking at how a particular feature of AWS CDK can help us enforce compliance of various resources we deploy to AWS.
Enter AWS CDK Aspects.
The code this blog post is based on lives in this GitHub repository
What are AWS CDK Aspects
AWS CDK Aspects is a feature of the AWS CDK framework that allows you to perform various operations on each node of the AWS CDK Construct tree.
Note that the Aspects code is invoked at the prepare phase of AWS CDK application lifecycle.
Imagine a scenario where you have to ensure that every AWS S3 bucket in the AWS CDK application you maintain has the BucketEncryption
property specified according to your organization's needs.
Instead of going bucket by bucket and applying the correct value for the BucketEncryption
property, one might use AWS CDK Aspects to programmatically set this property for all buckets deployed by the application.
With a basic overview of Aspects behind us, let us look at examples next.
Practical examples
What follows is a series of examples showcasing the usefulness of AWS CDK Aspects.
Enforcing AWS S3 bucket encryption
AWS S3 is an excellent service. Sadly, when misconfigured, it can be a source of a data breach. There are many stories out there regarding publicly accessible AWS S3 buckets with plain-text files that hold important data.
Thankfully, with AWS CDK Aspects, it is possible to reduce the human error factor regarding AWS S3 bucket configuration. The following is an example of ensuring that all the buckets within a Stack
have BucketEncryption
specified.
import { Aspects, aws_s3, IAspect, Stack, StackProps, App } from "aws-cdk-lib";
import { Construct, IConstruct } from "constructs";
export class UnencryptedBucket extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const unencryptedBucket = new aws_s3.Bucket(this, "UnencryptedBucket", {
/**
* Per documentation: `Kms` if `encryptionKey` is specified, or `Unencrypted` otherwise.
* The `undefined` is added for the sake of the example.
*/
encryption: undefined
});
}
}
class BucketEncryptionChecker implements IAspect {
public visit(node: IConstruct): void {
if (node instanceof aws_s3.CfnBucket) {
/**
* Feel free to be more specific in terms of conditions in your code.
*/
if (!node.bucketEncryption) {
throw new Error("`bucketEncryption` must be specified.");
}
}
}
}
class BucketStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
new UnencryptedBucket(this, "UnencryptedBucket");
}
}
const app = new App();
const bucketStack = new BucketStack(app, "BucketStack");
Aspects.of(bucketStack).add(new BucketEncryptionChecker());
Variable DeletionPolicy
The DeletionPolicy
attribute instructs AWS CloudFormation what should happen to the resource when AWS CloudFormation applies the "DELETE" action to that resource.
Imagine a scenario where someone removes the definition of a database from the template and pushes that Change Set to AWS CloudFormation – without the DeletionPolicy
specified to Retain
, AWS CloudFormation delete the resource and all the data along with it!
Conversely, imagine deploying emphermal stacks onto your development-only AWS account. You, most likely, would want all the resources gone whenever the stack is deleted – in such scenarios having the DeletionPolicy
as Delete
might be a wiser choice.
The following is an example of using AWS CDK Aspects to specify the DeletionPolicy
to Delete
for all resources when deploying the stack in a dev-only environment and to Retain
when deploying to a production environment.
import {
App,
Aspects,
CfnResource,
IAspect,
RemovalPolicy,
Stack,
StackProps
} from "aws-cdk-lib";
import { Construct, IConstruct } from "constructs";
export class DeletionPolicySetter implements IAspect {
constructor(private readonly policy: RemovalPolicy) {}
visit(node: IConstruct): void {
/**
* Nothing stops you from adding more conditions here.
*/
if (node instanceof CfnResource) {
node.applyRemovalPolicy(this.policy);
}
}
}
class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
/**
* Your code...
*/
}
}
const app = new App();
const myStack = new MyStack(app, "MyStack");
const removalPolicy =
process.env.DEPLOYMENT_ENV == "DEV"
? RemovalPolicy.DESTROY
: RemovalPolicy.RETAIN;
Aspects.of(myStack).add(new DeletionPolicySetter(removalPolicy));
Please be extra careful here. While, to my best knowledge, the change in the DeletionPolicy
is an "Update with No Interruption" the worst thing to do is to forget about setting the DeletionPolicy
to Delete
on resources holding production data.
Closing words
I hope that you found the examples helpful. I've been using Aspects in my personal projects for a while now, and I found them very useful.
Found some neat use-case for Aspects? Please share it! I'm always keen on learning new things from the community.
For more AWS CDK and serverless content, consider following me on Twitter - @wm_matuszewski
Thank you for your valuable time.