Map AWS Foundational Security Best Practices to TypeScript CDK checks
securityiacaws

Map AWS Foundational Security Best Practices to TypeScript CDK checks

JJordan Mitchell
2026-05-09
22 min read
Sponsored ads
Sponsored ads

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 controlAWS resource patternCDK / cdk-nag recipePR-time test approach
S3 buckets should block public accessaws_s3.BucketUse a secure wrapper construct; assert blockPublicAccess and no public ACLsTemplate assertion for public access block and bucket policy absence
CloudTrail should be enabledaws_cloudtrail.TrailShared security stack construct that creates an org trailSnapshot test plus cdk-nag rule requiring one trail per account
EC2 instances should use IMDSv2aws_ec2.Instance / LaunchTemplateAspect that checks metadata options httpTokens=requiredSynth test asserting metadata options on every instance/launch template
Security groups should not allow 0.0.0.0/0 on sensitive portsaws_ec2.SecurityGroupCustom nag rule for ingress CIDRs and port rangesUnit test enumerating ingress rules and rejecting wide-open exposure
ALB / API should have logging enabledaws_elasticloadbalancingv2 / aws_apigatewayOpinionated construct with access logs, retention, and KMS settingsTemplate inspection for log destination and encryption settings
RDS should encrypt storageaws_rds.DatabaseInstance / ClusterWrapper construct hardcodes encryption and deletion protectionAssertion 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.

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.

Advertisement
IN BETWEEN SECTIONS
Sponsored Content

Related Topics

#security#iac#aws
J

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.

Advertisement
BOTTOM
Sponsored Content
2026-05-09T00:07:40.932Z