CL:** Using TypeScript for IaC allows developers to leverage existing language skills, type checking, and object-oriented design. It enables the creation of secure-by-default patterns.
- cdk-nag for Construct Validation:
cdk-nag runs checks against the CDK construct tree before synthesis. It catches issues that static analysis of synthesized templates might miss, such as missing tags or improper resource associations.
- OPA for Policy Enforcement: Open Policy Agent (OPA) provides a declarative language (Rego) to define complex policies that span multiple resources, enforcing organizational standards independent of the IaC tool.
2. Implementation Steps
Step A: Define Secure Baseline with cdk-nag
Install cdk-nag and apply security packs to the CDK app. This enforces CIS AWS Foundations Benchmarks and custom rules automatically.
// lib/app.ts
import { App, Stack, StackProps } from 'aws-cdk-lib';
import { NagPackSuppression, NagPackSeverity } from 'cdk-nag';
import { AwsSolutionsChecks } from 'cdk-nag';
import { MySecureStack } from './secure-stack';
const app = new App();
const stack = new MySecureStack(app, 'ProdStack');
// Apply AWS Solutions security pack
AwsSolutionsChecks.check(app);
// Optional: Apply custom suppressions with justification
// new NagPackSuppression({ id: 'AwsSolutions-IAM4', reason: 'Legacy service requires broad access' });
Step B: Create a Secure S3 Bucket Construct
Encapsulate security logic in a custom construct. This ensures that every instance of the bucket inherits security controls, preventing configuration drift at the resource level.
// lib/constructs/secure-bucket.ts
import { Bucket, BucketEncryption, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export interface SecureBucketProps {
encryptionKeyArn?: string;
}
export class SecureBucket extends Construct {
public readonly bucket: Bucket;
constructor(scope: Construct, id: string, props: SecureBucketProps) {
super(scope, id);
this.bucket = new Bucket(this, 'SecureBucket', {
encryption: props.encryptionKeyArn
? BucketEncryption.KMS
: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
versioned: true,
enforceSSL: true,
});
// Explicitly deny public access policy
this.bucket.addToResourcePolicy(
new PolicyStatement({
actions: ['s3:*'],
principals: [new AnyPrincipal()],
effect: Effect.DENY,
resources: [this.bucket.arnForObjects('*')],
conditions: {
Bool: { 'aws:SecureTransport': 'false' },
},
})
);
}
}
Step C: Integrate OPA Policy-as-Code
Define a Rego policy to enforce that all S3 buckets must have versioning enabled and public access blocked. This serves as a secondary validation layer in CI.
# policies/s3-security.rego
package terraform.security
deny[msg] {
input.resource.aws_s3_bucket[*].versioning.enabled != true
msg := "S3 Bucket versioning must be enabled"
}
deny[msg] {
input.resource.aws_s3_bucket[*].block_public_acls != true
msg := "S3 Bucket public ACLs must be blocked"
}
deny[msg] {
input.resource.aws_s3_bucket[*].block_public_policy != true
msg := "S3 Bucket public policy must be blocked"
}
Step D: CI/CD Pipeline Integration
Configure the pipeline to run cdk-nag and OPA checks. The build fails if policies are violated.
# .github/workflows/iac-security.yml
name: IaC Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run cdk-nag
run: npx cdk synth && npx cdk-nag app.js
# cdk-nag exits with non-zero on violations
- name: Run OPA Check
uses: open-policy-agent/opa-action@v1
with:
rego: policies/s3-security.rego
input: cdk.out/manifest.json
fail: true
Architecture Rationale
- Type Safety: TypeScript catches reference errors and type mismatches during synthesis, preventing invalid infrastructure definitions from reaching the cloud.
- Encapsulation: Custom constructs abstract security complexity. Developers use
new SecureBucket() rather than configuring 15 separate properties, reducing human error.
- Defense in Depth:
cdk-nag catches construct-level issues, while OPA validates the synthesized plan. This dual-layer approach ensures that even if one tool misses a rule, the other catches it.
Pitfall Guide
1. Hardcoding Secrets in IaC
Mistake: Embedding API keys, passwords, or tokens directly in IaC files.
Impact: Secrets are committed to version control, accessible to anyone with repo access, and persist in git history even after deletion.
Best Practice: Use secret managers (AWS Secrets Manager, HashiCorp Vault). Reference secrets via dynamic references or environment variables injected at runtime. Never store secrets in state files without encryption.
2. Over-Permissive IAM Roles
Mistake: Assigning AdministratorAccess or wildcard * actions to IAM roles attached to Lambda functions or EC2 instances.
Impact: If a resource is compromised, the attacker gains full account control.
Best Practice: Implement least privilege. Use IAM Access Analyzer to generate policies based on actual activity. Define granular permissions in constructs.
3. Ignoring Drift Detection
Mistake: Assuming IaC state matches reality. Manual console changes create drift.
Impact: Security controls defined in code may not be enforced in the environment. Unmanaged resources become shadow IT vulnerabilities.
Best Practice: Run drift detection jobs nightly. Alert on discrepancies and enforce terraform plan or cdk diff checks before any deployment.
4. Using Unvetted Public Modules
Mistake: Importing community modules without auditing their code or pinning versions.
Impact: Supply chain attacks. Malicious modules can exfiltrate data or create backdoors. Version updates can introduce breaking security changes.
Best Practice: Fork and audit critical modules. Pin module versions to specific commits or hashes. Use private registries for approved modules.
5. Bypassing Security Checks in CI
Mistake: Allowing developers to skip checks using --force or disabling pipelines for "hotfixes."
Impact: Insecure infrastructure reaches production. Bypasses become habitual, rendering security tooling useless.
Best Practice: Make checks mandatory. Require justification for suppressions. Use branch protection rules to prevent merges without passing security gates.
6. State File Exposure
Mistake: Storing state files in unencrypted S3 buckets or public repositories.
Impact: State files contain resource IDs, configurations, and potentially secrets. Exposure allows attackers to map infrastructure and target specific resources.
Best Practice: Encrypt state files at rest and in transit. Enable versioning and locking. Restrict access to state backends via strict IAM policies.
7. Treating IaC Security as a One-Time Scan
Mistake: Running security scans only during initial setup or annual audits.
Impact: New vulnerabilities emerge in dependencies and cloud services. Security posture degrades over time.
Best Practice: Integrate continuous scanning. Update policy libraries regularly. Schedule periodic re-evaluation of infrastructure against updated benchmarks.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small Team / Startup | Checkov + Pre-commit Hooks | Low overhead, easy setup, covers common misconfigurations. | Low (Open source tools) |
| Enterprise / Regulated | OPA + cdk-nag + CI/CD Gates | Enforces complex policies, audit trails, strict compliance controls. | Medium (Engineering time for policies) |
| Multi-Cloud Environment | OPA + Infracost | Platform-agnostic policies, cost visibility, consistent standards. | Medium (Unified policy maintenance) |
| High Velocity / DevOps | CDK-Nag + TypeScript Constructs | Shift-left validation, type safety, developer-friendly security patterns. | Low (Leverages existing dev skills) |
Configuration Template
GitHub Actions Workflow for IaC Security
Copy this workflow to .github/workflows/iac-security.yml to enforce security checks on every push and PR.
name: Infrastructure Security Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Synthesize CDK
run: npx cdk synth
- name: Run cdk-nag Security Checks
run: |
npx cdk-nag cdk.out/*.json
# Fails build if violations found
- name: Run Checkov Static Analysis
uses: bridgecrewio/checkov-action@master
with:
directory: cdk.out
framework: terraform
quiet: true
soft_fail: false
# Fails build on critical/high vulnerabilities
- name: Upload Security Report
if: always()
uses: actions/upload-artifact@v3
with:
name: security-report
path: reports/
Quick Start Guide
- Install Tools: Run
npm install --save-dev cdk-nag checkov in your IaC project directory.
- Add Security Pack: Import
AwsSolutionsChecks in your CDK app entry point and call AwsSolutionsChecks.check(app).
- Configure Pre-commit: Create a
.pre-commit-config.yaml file with hooks for checkov and ts-node (to run local CDK synthesis and nag checks).
- Run Local Scan: Execute
pre-commit run --all-files to identify and fix initial violations.
- Enable CI Pipeline: Add the provided GitHub Actions workflow to enforce checks on all future changes.
Infrastructure security is not a feature; it is a property of the development process. By embedding validation into the code lifecycle and enforcing policies at the construct level, you transform security from a reactive audit into a proactive guarantee. Implement these patterns to harden your infrastructure against misconfiguration, drift, and supply chain threats.