Map AWS Foundational Security Best Practices to TypeScript CDK checks
Turn AWS FSBP controls into TypeScript CDK and cdk-nag checks so compliance fails fast in PRs, not after deployment.
If you are using AWS CDK in TypeScript, the fastest way to improve cloud security is to push Foundational Security Best Practices left into pull requests. That means translating AWS Security Hub FSBP controls into CDK constructs, custom assertions, and cdk-nag rules so developers see misconfigurations before anything reaches a live account. This guide shows how to turn policy into code for the controls that matter most, with concrete recipes you can drop into your pipelines today. If you are also standardizing broader governance and change management, it helps to think of this as part of your wider compliance automation strategy, not just a one-off linting exercise.
A practical mapping layer is more useful than trying to mirror every Security Hub control verbatim. In real teams, the winning pattern is to enforce what can be validated at synth time, test what needs CloudFormation semantics, and defer only service-runtime checks to Security Hub after deploy. That approach aligns with the discipline used in robust supply-chain hygiene for dev pipelines: catch issues early, encode rules once, and make exceptions explicit.
1. Why map FSBP into CDK instead of relying only on Security Hub?
Shift security from detection to prevention
Security Hub is excellent at finding drift and verifying posture in deployed AWS accounts. But by the time a finding appears there, you have already merged code, built artifacts, and possibly exposed a resource. CDK gives you a stronger lever because it defines infrastructure before it exists, which means you can block insecure defaults in the same PR that introduces them. This is the essence of policy-as-code: security is versioned, reviewed, and automated alongside application code.
That shift matters because many FSBP controls are predictable and deterministic. Examples include encryption settings, logging flags, public accessibility, TLS configuration, and metadata service hardening. Those are exactly the kinds of rules CDK can assert with confidence. For teams trying to scale governance across many repos, the model is similar to the way enterprises scale AI beyond pilots: create reusable guardrails, apply them consistently, and measure adoption instead of relying on manual review alone, as described in our guide to scaling AI across the enterprise.
What CDK can validate better than Security Hub
CDK can detect intent directly in code. For example, you can check whether an S3 bucket is created with encryption, whether an API Gateway stage has access logging, whether an ECS task definition enables read-only root filesystems, or whether a security group has open ingress from 0.0.0.0/0. This is more actionable than a generic account-level alert because developers can see the exact line that introduced the risk.
It is also easier to enforce organizational standards in reusable constructs. If every team consumes a shared SecureBucket or PrivateRestApi construct, then a single change can propagate across dozens of services. That same idea shows up in other design systems and platforms, where a strong foundation reduces downstream entropy, much like how strong page structures help build page-level authority rather than chasing surface-level metrics.
What still belongs in Security Hub
Not every control can or should be enforced in CDK. Some controls depend on runtime behavior, cross-account state, or console actions outside infrastructure code. Examples include security contact information, certificates nearing expiration, or some organization-level posture checks. Those are still valuable in Security Hub and should remain part of your detection layer. The most mature programs use both: CDK for prevention, Security Hub for verification, and tickets or ChatOps for remediation.
2. How to think about FSBP controls as three enforcement layers
Layer 1: CDK construct defaults
The cleanest enforcement point is the construct itself. If you wrap a risky AWS service in a higher-level TypeScript construct, you can make the secure path the default path. For example, a bucket construct can require encryption, block public access, enable access logs, and tag resources consistently. A load balancer construct can enforce TLS policies, access logging, and deletion protection. The closer the control is to the primitive, the easier it is to standardize across teams.
This is where your codebase becomes a policy product. Instead of telling every team to remember a checklist, you ship a secure abstraction that encodes the checklist. That pattern resembles the way platform teams design localized infrastructure offerings in other domains, such as reference architectures for hosting providers: the consumer gets a safer, simpler interface, while the platform team keeps control of the hard parts.
Layer 2: cdk-nag rules
Some teams cannot immediately swap to opinionated constructs. In those cases, cdk-nag is the ideal bridge because it inspects synthesized CDK resources and flags violations. You can adopt built-in packs where they match your needs, then add custom rules for the exact FSBP controls you care about. That lets you support legacy stacks, heterogeneous ownership models, and gradual migration without giving up enforceability.
Think of cdk-nag as a unit-test framework for infrastructure intent. If a stack violates a rule, developers see the failure during synthesis or test execution, not after deployment. That is especially helpful in larger organizations where dozens of teams own their own stacks and coordination overhead is high. It is the infrastructure equivalent of turning audience signals into growth actions, similar to how creators use retention data to improve outcomes before the platform metrics tell the story.
Layer 3: Security Hub as the backstop
Finally, Security Hub remains the system of record for account posture. Even a perfect CDK rule set cannot cover out-of-band changes, imported legacy resources, or manual console actions. Security Hub gives you continuous detection across the whole account, so your preventive controls and detective controls reinforce each other. The goal is not to choose one; it is to make each layer do what it does best.
3. FSBP controls that map cleanly to TypeScript CDK checks
High-confidence controls you can enforce in code
Many FSBP controls have a nearly one-to-one mapping to CDK configuration. Examples include encryption at rest, public access blocking, logging, TLS enforcement, IMDSv2, and least-privilege authorization settings. These are ideal candidates for Aspects, custom IAspect implementations, or cdk-nag rules because they produce deterministic outcomes when the stack is synthesized. The more deterministic the control, the more valuable it is to fail the build early.
The table below highlights common controls, the AWS resource pattern, a CDK enforcement strategy, and how to test it. The intent is to show how to turn FSBP from audit language into concrete implementation work.
| FSBP control | AWS resource pattern | CDK / cdk-nag recipe | PR-time test approach |
|---|---|---|---|
| S3 buckets should block public access | aws_s3.Bucket | Use a secure wrapper construct; assert blockPublicAccess and no public ACLs | Template assertion for public access block and bucket policy absence |
| CloudTrail should be enabled | aws_cloudtrail.Trail | Shared security stack construct that creates an org trail | Snapshot test plus cdk-nag rule requiring one trail per account |
| EC2 instances should use IMDSv2 | aws_ec2.Instance / LaunchTemplate | Aspect that checks metadata options httpTokens=required | Synth test asserting metadata options on every instance/launch template |
| Security groups should not allow 0.0.0.0/0 on sensitive ports | aws_ec2.SecurityGroup | Custom nag rule for ingress CIDRs and port ranges | Unit test enumerating ingress rules and rejecting wide-open exposure |
| ALB / API should have logging enabled | aws_elasticloadbalancingv2 / aws_apigateway | Opinionated construct with access logs, retention, and KMS settings | Template inspection for log destination and encryption settings |
| RDS should encrypt storage | aws_rds.DatabaseInstance / Cluster | Wrapper construct hardcodes encryption and deletion protection | Assertion on storageEncrypted and backup retention |
These examples are deliberately simple because the win is consistency, not cleverness. Security teams often overcomplicate controls, but developers adopt rules faster when the failure message is obvious and the fix is local. If you want to build a broader platform around this, the same logic applies to secure defaults and lifecycle controls in other technical domains, such as the practical hardening lessons in our article on what to do when updates go wrong.
Controls that need a hybrid approach
Some FSBP items are partially enforceable in code but still require runtime validation. Certificate expiration is a good example: you can enforce that an ACM certificate is imported correctly and used in the right place, but expiry is time-based and belongs in Security Hub or scheduled checks. Another example is API logging: CDK can ensure log destinations and settings are present, but you may still want post-deploy verification that log delivery is flowing as expected.
The hybrid model is where compliance automation becomes practical rather than aspirational. Your code prevents predictable failures, and your detective layer catches the rest. This is the same logic behind good operational resilience: validate, monitor, and keep a separate alerting path for issues that are impossible to prove statically.
Controls that usually remain detective-only
Some controls are about organizational configuration, user behavior, or external state. Security contact information, some account settings, and resource age checks are typically not ideal CDK targets. You should still track them in your security program, but don't waste engineering time trying to force them into infrastructure code if the signal is naturally account-level or temporal. The right outcome is a clearly defined ownership model for each control, not a theoretical one-size-fits-all implementation.
4. Building reusable secure constructs in TypeScript
Use opinionated wrappers, not raw primitives
The most durable pattern is to create secure wrapper constructs around risky AWS resources. For example, rather than exposing new s3.Bucket() to application teams directly, create SecureBucket with mandatory encryption, versioning, block public access, access logs, and lifecycle rules. The same idea works for SecureRestApi, SecureQueue, and SecureDatabase. Each wrapper should encode your organization’s baseline and expose only safe customization points.
This is a classic platform-engineering move: reduce decision fatigue by making the secure path the easy path. The benefit compounds over time because every new stack inherits the baseline automatically. For teams that already practice modular service design, this will feel similar to strong domain boundaries in application architecture, where you minimize accidental complexity by hiding the dangerous parts behind a reliable interface.
Example: secure S3 bucket construct
import { Bucket, BucketEncryption, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export interface SecureBucketProps {
bucketName?: string;
}
export class SecureBucket extends Bucket {
constructor(scope: Construct, id: string, props: SecureBucketProps = {}) {
super(scope, id, {
bucketName: props.bucketName,
encryption: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
versioned: true,
enforceSSL: true,
objectOwnership: undefined,
});
}
}This example does not solve every S3-related control, but it eliminates the most common and most dangerous mistakes. In practice, you would extend it with logging, KMS encryption, lifecycle policies, and perhaps a compliance tag so Security Hub findings can be correlated back to the construct owner. Good secure defaults are rarely fancy, but they are highly effective when they are used everywhere.
Example: secure API Gateway construct
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';
export class SecureRestApi extends apigateway.RestApi {
constructor(scope: Construct, id: string) {
super(scope, id, {
deployOptions: {
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: false,
accessLogDestination: new apigateway.LogGroupLogDestination(/* log group */),
accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields(),
tracingEnabled: true,
},
cloudWatchRole: true,
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
},
});
}
}API Gateway controls are a good example of where a wrapper can encode multiple FSBP concerns at once. You can enforce logging, tracing, TLS posture, and auth defaults in one place. Teams that are already careful with service boundaries, similar to the way accessible API patterns are designed for older adults, tend to adopt these patterns faster because the developer experience remains clear.
5. Writing cdk-nag rules for the controls you cannot encode in constructs
Build custom nag packs in TypeScript
cdk-nag is especially useful when you have to police existing stacks or mixed ownership models. A custom nag pack can inspect low-level CloudFormation resources and emit warnings or errors based on the synthesized template. That means you can start with the infrastructure you have, not the ideal infrastructure you wish you had. Over time, teams can eliminate findings by moving to secure constructs instead of relying on exceptions.
A simple custom rule usually checks a property on a generated resource and compares it against your baseline. For example, a rule can reject S3 buckets without encryption or EC2 instances without IMDSv2. The key is to write meaningful messages that tell developers exactly what to change. The more specific the fix, the faster the culture shifts from “security blocked me” to “security told me what to do.”
Example: detect EC2 instances without IMDSv2
import { IConstruct } from 'constructs';
import { Annotations, CfnResource } from 'aws-cdk-lib';
export function enforceImdsv2(scope: IConstruct) {
const resources = scope.node.findAll().filter((c): c is CfnResource => c instanceof CfnResource);
for (const resource of resources) {
if (resource.cfnResourceType === 'AWS::EC2::Instance') {
const metadataOptions = resource.cfnProperties?.MetadataOptions;
if (metadataOptions?.HttpTokens !== 'required') {
Annotations.of(resource).addError('FSBP: EC2 instances must require IMDSv2 (HttpTokens=required).');
}
}
}
}For production use, wrap this as a proper aspect or cdk-nag rule rather than a loose helper function. That gives you a repeatable mechanism and allows your rule to integrate with the rest of your policy library. If you want to deepen your implementation discipline, it helps to study how teams design resilient systems under uncertainty, like the simulation-first approach in sim-to-real deployment pipelines.
Example: reject wide-open security groups
import { Annotations } from 'aws-cdk-lib';
import { SecurityGroup, CfnSecurityGroupIngress } from 'aws-cdk-lib/aws-ec2';
export function rejectWideOpenIngress(securityGroup: SecurityGroup) {
for (const child of securityGroup.node.findAll()) {
if (child instanceof CfnSecurityGroupIngress) {
const cidr = child.cidrIp;
const port = child.fromPort;
if (cidr === '0.0.0.0/0' && (port === 22 || port === 3389 || port === 443)) {
Annotations.of(child).addError('FSBP: Do not allow public ingress on sensitive ports.');
}
}
}
}Security groups are a classic place where teams accidentally normalize risk. Once a wide-open ingress rule lands in one service, it tends to spread through copy-paste. Cdk-nag helps stop that pattern early, and in larger programs it can be paired with metrics and ownership data so platform teams can see which services still need remediation. This is similar to how analysts use tracking analytics to move from anecdote to actionable insight.
6. PR-time testing patterns that make compliance real
Use synth tests, assertions, and snapshots together
The strongest PR gate is not a single test style; it is a layered test strategy. Use CDK assertions for deterministic properties, snapshot tests for broad regression coverage, and cdk-nag for policy violations. That combination gives you fast feedback for developers and stable enforcement for the platform team. The important part is to keep the failures understandable, so people know whether they need to fix a construct, a stack, or a policy rule.
In TypeScript, a typical test file can synthesize a stack and inspect the generated CloudFormation template. This is especially effective for controls like encryption, logging, public access, and authorization. If your team already values workflow tooling and CI robustness, treat these tests like any other critical build artifact: they should be run on every pull request and fail the build when controls are broken.
Example: assert an encrypted S3 bucket
import { App, Stack } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { SecureBucket } from '../lib/secure-bucket';
test('bucket is encrypted and private', () => {
const app = new App();
const stack = new Stack(app, 'TestStack');
new SecureBucket(stack, 'Bucket');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: Match.anyValue(),
});
template.hasResource('AWS::S3::Bucket', Match.objectLike({
DeletionPolicy: 'Retain',
}));
});This style is easy to read and easy to maintain. It is also much more actionable than waiting for a Security Hub finding after deployment. For teams used to shipping consumer-facing products, this feels like the difference between validating a release in staging and learning about a broken experience from users later, which is a lesson familiar to anyone who has studied offline-first product design.
Example: fail builds on cdk-nag findings
import { App, Aspects, Stack } from 'aws-cdk-lib';
import { NagSuppressions } from 'cdk-nag';
import { AwsSolutionsChecks } from 'cdk-nag';
const app = new App();
const stack = new Stack(app, 'SecurityStack');
Aspects.of(stack).add(new AwsSolutionsChecks({ verbose: true }));
// Add explicit suppressions only with ticket references and expiration.
NagSuppressions.addStackSuppressions(stack, [
{
id: 'AwsSolutions-S1',
reason: 'Centralized logging is in a shared account. Ticket SEC-1234.',
},
]);Suppressions are not the enemy, but they must be treated as controlled exceptions. Every suppression should have an owner, a reason, and ideally an expiry or review date. That governance pattern keeps exceptions from turning into silent policy erosion. It is the same operating principle that makes high-trust teams successful in other regulated workflows, including building trust after misconduct: clarity and accountability matter more than wishful thinking.
7. A practical control-by-control mapping playbook
S3, CloudFront, and KMS
Start with services that are both common and easy to standardize. S3 buckets should be private, encrypted, and versioned by default. CloudFront distributions should forward minimal headers and enforce TLS. KMS keys should be managed with appropriate rotation, access policies, and usage constraints. These controls are among the highest-value because they reduce exposure across many workloads without requiring deep application changes.
For S3, wrap the bucket and test its properties. For CloudFront, create a construct that enforces a modern security policy and access logging. For KMS, make it hard to create unconstrained keys by giving developers a pattern that already includes the right defaults. Once those guardrails exist, you can extend the same method to queues, databases, and secrets.
EC2, Auto Scaling, and ECS
For EC2 and Auto Scaling, enforce IMDSv2, avoid public IPs on private workloads, and require multiple Availability Zones where high availability matters. For ECS, ensure task definitions do not overexpose network ports, use the right execution role permissions, and prefer encrypted log delivery. These are ideal candidates for policy-as-code because misconfiguration often comes from copy-paste and rushed delivery, not malicious intent.
Controls like these map well to higher-order constructs that encode operational safety. If your team is modernizing platform patterns, the difference between a raw resource and an opinionated construct is similar to the difference between a generic tool and a professional workflow, much like how creators choose better tooling after comparing enterprise AI versus consumer chatbots.
API Gateway, AppSync, and Lambda
For API Gateway, turn on execution logging, access logs, auth defaults, and tracing where appropriate. For AppSync, avoid API keys for production auth and ensure logging is intentional. For Lambda, focus on least-privilege IAM, environment variable encryption, and network exposure when functions are attached to VPCs. These services are often used as public edges, so your CDK standards should be especially strict.
One effective pattern is to maintain service-specific compliance helpers. For example, a SecureGraphqlApi construct can enforce auth modes, logging, and cache settings. A SecureFunction helper can mandate environment encryption and sane timeout defaults. That way teams get secure scaffolding without needing to memorize the entire FSBP catalog.
8. Operationalizing the policy library across teams
Publish a shared compliance package
Once your rules are stable, package them as an internal library. In practice, that means a TypeScript package that exports secure constructs, Nag packs, aspects, and utility assertions. Version it carefully, document the baseline, and make breaking changes explicit. The goal is to provide a single source of truth for infrastructure security defaults.
This is the point where engineering productivity and security begin to reinforce each other. Teams spend less time arguing about standards and more time shipping systems that already comply. It is also a good place to add examples for common edge cases, because well-placed examples reduce support burden more than prose alone. If you need a model for creating a clear operating system around a complex problem, think of the way creators use structured research templates to prototype offers in research-driven product work.
Make exceptions visible and reviewable
Every security policy eventually meets a real-world exception. The mistake is not having exceptions; the mistake is making them invisible. Store suppressions alongside code, require ticket references, and review them periodically. Better yet, emit metrics on the number of suppressions per team so you can tell whether adoption is improving or whether teams are bypassing the guardrails too often.
Exception hygiene is to security policy what inventory hygiene is to operations: if you do not monitor it, drift becomes normal. Teams that are good at governance tend to understand this instinctively, whether they manage warehouses, releases, or cloud resources. That is why a disciplined platform program should treat suppressions as temporary agreements, not permanent entitlements.
Measure compliance as engineering telemetry
Do not stop at pass/fail. Track the number of findings per release, the average time to fix, the percentage of stacks using secure constructs, and the number of false positives. Those metrics help you decide where to invest next. If one rule produces a high rate of suppressions, the rule might be too noisy or the abstraction too hard to use. If another rule catches frequent mistakes with near-zero friction, prioritize it for broader rollout.
Pro tip: The best compliance programs do not ask, “Did the scan find anything?” They ask, “Did the developer experience make the safe choice the easy choice?” If the answer is yes, adoption grows naturally.
9. A recommended implementation roadmap
Phase 1: protect the top 10 controls
Do not try to encode the entire FSBP catalog in one pass. Start with the controls that are common, risky, and easy to prove in CDK: encryption, public access blocking, logging, TLS, IMDSv2, and security group hygiene. Those controls will eliminate a large share of accidental exposure with relatively little effort. Early wins matter because they create trust in the platform approach.
As you stabilize the first wave, add service-specific rules for the resources your teams use most. A small, highly adopted policy package is better than a giant one that nobody trusts. This is a standard pattern in mature engineering organizations: narrow first, then expand based on usage and feedback.
Phase 2: add PR gating and dashboards
Once the rules are stable, fail pull requests when violations occur. Pair that with a dashboard that shows the trend over time, the most common offenses, and the teams that need support. The dashboard is not for shaming; it is for prioritization. It helps security and platform teams choose where to invest their next hour.
If your organization already uses compliance or governance reporting, connect the PR signal to downstream evidence. This creates a clean story from code review to deployment to runtime posture. It also reduces the chance that teams dismiss security controls as abstract corporate policy rather than a practical engineering tool.
Phase 3: close the loop with Security Hub
Finally, ensure Security Hub remains the runtime backstop. Correlate findings back to the stack, construct, or rule that should have prevented them. When you see drift, treat it as input to improve your CDK rules or shared constructs. That feedback loop is where policy-as-code matures from a compliance checkbox into a real engineering system.
10. Conclusion: make compliance a developer experience feature
Put the control where the developer already works
The deepest value in mapping FSBP to CDK is not the audit trail; it is the developer experience. Developers already live in TypeScript, pull requests, and CI output, so that is where your policies should live too. When the security baseline becomes code, you lower the cognitive burden on every team and dramatically reduce the number of avoidable findings. Security becomes part of the build, not a surprise at the end.
Use both prevention and detection
CDK and cdk-nag are your prevention layer. Security Hub is your detection layer. Together they create a stronger, more resilient operating model than either one alone. If you implement them thoughtfully, you get safer defaults, faster reviews, cleaner audits, and fewer production surprises.
Start small, standardize hard
Pick a handful of high-value FSBP controls, implement them as secure constructs or custom nag rules, and enforce them in PRs. Then expand gradually, guided by usage data and real-world findings. That combination of discipline and pragmatism is what turns compliance into a competitive advantage rather than a tax.
For teams building long-term cloud platforms, this is the same lesson that appears across other resilient systems: strong defaults matter, feedback loops matter, and reusable abstractions beat one-off heroics. The result is a codebase that is easier to review, easier to trust, and far more likely to stay secure as it grows.
Frequently Asked Questions
Can every AWS Security Hub FSBP control be enforced in CDK?
No. Many controls can be enforced directly at synth time, especially those involving resource properties like encryption, logging, and public exposure. Others depend on runtime state, external services, or account-level configuration and are better left to Security Hub or scheduled audits. A hybrid model is the most realistic and maintainable approach.
Should I use secure constructs or cdk-nag first?
If you control the platform and can influence new code, start with secure constructs because they make the safe path the default. If you need to govern existing stacks or heterogeneous teams, start with cdk-nag because it works on synthesized output without requiring immediate refactoring. Most mature teams eventually use both.
How do I prevent cdk-nag suppressions from becoming loopholes?
Require a reason, an issue ticket, and an owner for every suppression. Review suppressions periodically, and consider expiry dates or scheduled revalidation. If suppressions rise quickly, treat that as a signal that your rule is too noisy or your abstraction is too hard to use.
What is the best way to test FSBP mappings in pull requests?
Use a mix of CDK assertions, snapshot tests, and cdk-nag. Assertions are best for exact resource properties, snapshots are useful for regression coverage, and cdk-nag is ideal for policy enforcement. Together they give you both precision and breadth.
How do I roll this out across many teams without slowing delivery?
Start with a small set of high-value controls, package them in a shared library, and document the developer experience carefully. Make the rules easy to understand and the fix easy to apply. The more your policy layer behaves like a well-designed toolkit, the less friction it creates.
Related Reading
- Supply-chain hygiene for dev pipelines - Practical lessons on keeping build systems trustworthy.
- The integration of AI and document management: a compliance perspective - Useful framing for auditability and controlled workflows.
- Scaling AI across the enterprise - A blueprint for reusable guardrails at scale.
- On-device AI appliances reference architecture - How strong defaults simplify complex platform delivery.
- Building page-level authority - A helpful analogy for designing durable systems that earn trust.
Related Topics
Jordan Mitchell
Senior SEO Content Strategist
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you
Build a model-agnostic TypeScript code-review agent inspired by Kodus
Integrating Kodus AI with TypeScript monorepos: practical patterns

Building platform-friendly web tools for PCB review with TypeScript and WebAssembly
From PCB to Dashboard: Building EV electronics monitoring tools with TypeScript
Building a TypeScript test harness around Kumo: typed fixtures, retries and persistent state
From Our Network
Trending stories across our publication group