Leveraging the Builder Pattern in security engineering to architect robust, scalable, and easy-to-maintain security controls
https://refactoring.guru/design-patterns/builder

Leveraging the Builder Pattern in security engineering to architect robust, scalable, and easy-to-maintain security controls

In the rapidly evolving landscape of cloud-native applications, security engineering teams are often tasked with building and maintaining protective controls that not only block threats but also scale, adapt, and integrate seamlessly into CI/CD workflows.

One approach that has proven effective in application development — and is increasingly relevant to infrastructure and security engineering — is the Builder Pattern.

💡 Why Use the Builder Pattern in Security Engineering?

The builder pattern is a well-established software design pattern used to construct complex objects step by step. When applied to security infrastructure, this pattern encourages:

  • Modularity – Break down security configurations into reusable, testable components.
  • Readability – Declarative, chained interfaces that make security intent clear.
  • Extensibility – Easy inclusion of new rules, controls, or detection logic without refactoring.
  • Consistency – Common security baselines enforced across multiple environments or teams.

🔐 Practical Applications

Whether you’re deploying firewalls, web access controls, or telemetry pipelines, a builder approach helps you move from handcrafted security scripts to reusable blueprints that can evolve.

⚙️ CDK Example: Web ACL Builder in TypeScript

Let’s say you want to define a reusable construct for AWS WAF using CDK. Here’s a class that lets you chain best-practice rules, add custom logic, and build the final WebACL.

import { aws_wafv2 as wafv2 } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export interface WafWebAclBuilderProps {
  name: string;
  scope: 'CLOUDFRONT' | 'REGIONAL';
  defaultAction?: wafv2.CfnWebACL.DefaultActionProperty;
  visibilityConfig?: wafv2.CfnWebACL.VisibilityConfigProperty;
}

export class WafWebAclBuilder extends Construct {
  private readonly rules: wafv2.CfnWebACL.RuleProperty[] = [];

  constructor(scope: Construct, id: string, private readonly props: WafWebAclBuilderProps) {
    super(scope, id);
  }

  public addBestPracticeManagedRules(): this {
    this.rules.push(
      this.managedRule('AWSManagedRulesCommonRuleSet', 'AWS'),
      this.managedRule('AWSManagedRulesKnownBadInputsRuleSet', 'AWS'),
      this.managedRule('AWSManagedRulesAmazonIpReputationList', 'AWS')
    );
    return this;
  }

  private managedRule(name: string, vendor: string): wafv2.CfnWebACL.RuleProperty {
    return {
      name,
      priority: this.rules.length,
      overrideAction: { none: {} },
      statement: {
        managedRuleGroupStatement: {
          name,
          vendorName: vendor,
        },
      },
      visibilityConfig: {
        sampledRequestsEnabled: true,
        cloudWatchMetricsEnabled: true,
        metricName: name,
      },
    };
  }

  public addRule(rule: wafv2.CfnWebACL.RuleProperty): this {
    this.rules.push({ ...rule, priority: this.rules.length });
    return this;
  }

  public addCustomRuleFromJson(ruleJson: any): this {
    const rule: wafv2.CfnWebACL.RuleProperty = {
      ...ruleJson,
      priority: this.rules.length,
    };
    this.rules.push(rule);
    return this;
  }

  public build(): wafv2.CfnWebACL {
    return new wafv2.CfnWebACL(this, `${this.props.name}-WebACL`, {
      name: this.props.name,
      scope: this.props.scope,
      defaultAction: this.props.defaultAction ?? { allow: {} },
      visibilityConfig: this.props.visibilityConfig ?? {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: `${this.props.name}-ACL`,
      },
      rules: this.rules,
    });
  }
}


And here's how you'd use it:

new WafWebAclBuilder(this, 'AppWaf', {
  name: 'AppFirewall',
  scope: 'REGIONAL',
})
  .addBestPracticeManagedRules()
  .addCustomRuleFromJson({
    name: 'BlockSQLInjectionURIs',
    overrideAction: { none: {} },
    statement: {
      byteMatchStatement: {
        searchString: 'union select',
        fieldToMatch: { uriPath: {} },
        textTransformations: [{ priority: 0, type: 'LOWERCASE' }],
        positionalConstraint: 'CONTAINS',
      },
    },
    visibilityConfig: {
      sampledRequestsEnabled: true,
      cloudWatchMetricsEnabled: true,
      metricName: 'BlockSQLInjectionURIs',
    },
  })
  .build();

        


🌍 Terraform Example: Declarative WAF Module with Best Practice Rules

Here’s the equivalent in Terraform — a reusable module that accepts predefined AWS-managed rule sets and allows security teams to declare them centrally

✅ Step 1: locals.tf – Define Built-In Rules + Merge Strategy
locals {
  default_best_practice_rules = [
    {
      name            = "AWSManagedRulesCommonRuleSet"
      priority        = 0
      rule_group_name = "AWSManagedRulesCommonRuleSet"
      vendor_name     = "AWS"
      metric_name     = "CommonRuleSet"
    },
    {
      name            = "AWSManagedRulesKnownBadInputsRuleSet"
      priority        = 1
      rule_group_name = "AWSManagedRulesKnownBadInputsRuleSet"
      vendor_name     = "AWS"
      metric_name     = "KnownBadInputs"
    },
    {
      name            = "AWSManagedRulesAmazonIpReputationList"
      priority        = 2
      rule_group_name = "AWSManagedRulesAmazonIpReputationList"
      vendor_name     = "AWS"
      metric_name     = "IpReputation"
    }
  ]

  all_rules = [
    for i, rule in concat(local.default_best_practice_rules, var.additional_rules) : {
      name            = rule.name
      priority        = i
      rule_group_name = rule.rule_group_name
      vendor_name     = rule.vendor_name
      metric_name     = rule.metric_name
    }
  ]
}

 Step 2: variables.tf – Accept Only Add-Ons

variable "additional_rules" {
  type = list(object({
    name            = string
    rule_group_name = string
    vendor_name     = string
    metric_name     = string
  }))
  default = []
}


✅ Step 3: main.tf – Use local.all_rules
resource "aws_wafv2_web_acl" "this" {
  name        = var.name
  description = var.description
  scope       = var.scope

  default_action {
    allow {}
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "${var.name}-waf"
    sampled_requests_enabled   = true
  }

  dynamic "rule" {
    for_each = local.all_rules
    content {
      name     = rule.value.name
      priority = rule.value.priority

      override_action {
        none {}
      }

      statement {
        managed_rule_group_statement {
          name        = rule.value.rule_group_name
          vendor_name = rule.value.vendor_name
        }
      }

      visibility_config {
        sampled_requests_enabled   = true
        cloudwatch_metrics_enabled = true
        metric_name                = rule.value.metric_name
      }
    }
  }

  tags = var.tags
}

✅ Step 4: Root main.tf – Simple and Clean Usage

module "waf_best_practice_acl" {
  source      = "./waf_web_acl"
  name        = "my-waf-acl"
  description = "WAF ACL with best practice managed rules"
  scope       = "REGIONAL"

  # Optional: Add custom rules on top
  additional_rules = [
    {
      name            = "CustomAppProtectionRuleSet"
      rule_group_name = "AWSManagedRulesLinuxRuleSet"
      vendor_name     = "AWS"
      metric_name     = "LinuxProtection"
    }
  ]

  tags = {
    Environment = "prod"
    Owner       = "Security"
  }
}

        

🚀 Benefits for Security Teams

  • Scale across teams – Security engineers can define guardrails once and apply them consistently.
  • Simplify onboarding – New teams can adopt predefined builder modules without needing deep infrastructure knowledge.
  • Adapt faster – As threat landscapes change, shared builder logic can be updated centrally and propagated automatically.

🧱 Not Just Code – But a Mindset

Builder patterns are not just about pretty syntax — they represent a shift in how security capabilities are designed, shared, and evolved. When integrated with your infrastructure-as-code strategy (Terraform, Pulumi, CDK, etc.), builder-based security modules become powerful building blocks for resilient systems.


🔁 Security engineering deserves the same design discipline as application code. The builder pattern helps bridge that gap.

If you're exploring ways to improve the maintainability and scalability of your cloud security posture, consider starting with a builder mindset.


🗣️ Let's Discuss

Have you applied builder patterns or modular security design in your organisation? I’d love to hear your experience — challenges, patterns, and lessons learned.


To view or add a comment, sign in

More articles by Peter A.

Others also viewed

Explore content categories