Guardrails Before Accounts: An SCP Allow-List, Control Tower as Code, and Detection from Day One

  • AWS Organizations
  • SCPs
  • Control Tower
  • GuardDuty
  • Security Hub
  • Microsoft Sentinel

Before

Account onboarding was manual and error-prone.

What I built

SCP service allow-list (60+ approved services, all others denied), 50+ Control Tower controls as code, and GuardDuty, Security Hub and Config enabled for every new account, with findings forwarded to Microsoft Sentinel.

01New accountauto-enrolled02SCP allow-listdeny by default03Control Towercontrols as code04DetectionGuardDuty · Hub · Config05SIEMMicrosoft SentinelEvery account starts governed: nothing toremember to switch on.

Production work described at the level I can share publicly. Code samples are illustrative patterns, not production code; identifiers are documentation placeholders.

The problem: governance by hand

  • AWS Control Tower was enabled, but its controls were managed by hand in the console: no code record of which control applied to which organizational unit, and no review process for adding or removing one.
  • Any AWS service could be used in any account, including services nobody had assessed for security or compliance.
  • New accounts took about a week to provision through an inconsistent manual process, and each new account was a chance for a control to be forgotten.

The approach: an account is born governed

Every account gets the same guardrails and the same detection at creation, from code:

New account request
   → Account factory (Python)
   → Placed in an OU with the SCP allow-list and tag policies
   → Control Tower preventive and detective controls (from code)
   → GuardDuty + Security Hub + AWS Config (delegated admin, auto-enrolled)
   → Findings → Microsoft Sentinel

What I built

1. An SCP service allow-list

An organization-level SCP that allows 60+ approved AWS service namespaces and denies everything else on all non-sandbox accounts, with VPC endpoint enforcement and encryption-at-rest requirements. The OU structure and tag policies are managed as code alongside it.

Illustrative: the shape of an allow-list SCP (shortened).

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowApprovedServicesOnly",
      "Effect": "Allow",
      "Action": [
        "ec2:*", "s3:*", "kms:*", "iam:*", "sts:*",
        "lambda:*", "logs:*", "cloudwatch:*", "rds:*", "eks:*"
      ],
      "Resource": "*"
    }
  ]
}

For an allow-list to take effect, it replaces the default FullAWSAccess policy on the OUs it governs. Anything not listed is then implicitly denied.

2. Control Tower as code

50+ preventive and detective controls defined in Terraform across service categories including API Gateway, CloudTrail, CloudFront, DynamoDB, EC2, EKS, IAM, KMS, Lambda, RDS and S3. They apply to production and test OUs, with sandbox and security OUs excluded on purpose. Adding or removing a control is now a reviewed code change.

3. Detection for every new account

GuardDuty (including Lambda Protection), Security Hub and AWS Config are enabled through delegated administration, so every new account is enrolled in all active regions when it's created. Findings go to Microsoft Sentinel, the same SIEM that receives the Azure firewall logs, so both clouds are triaged in one place.

Illustrative: GuardDuty delegated admin and auto-enrolment of new accounts.

# In the management account: hand GuardDuty administration to the security account.
resource "aws_guardduty_organization_admin_account" "this" {
  admin_account_id = "111122223333"   # placeholder: security account
}

# In the security (delegated admin) account: enrol new member accounts automatically.
resource "aws_guardduty_organization_configuration" "this" {
  detector_id                      = aws_guardduty_detector.this.id
  auto_enable_organization_members = "NEW"
}

resource "aws_guardduty_organization_configuration_feature" "lambda" {
  detector_id = aws_guardduty_detector.this.id
  name        = "LAMBDA_NETWORK_LOGS"
  auto_enable = "NEW"
}

4. A faster account factory

A Python account factory cut provisioning from one week to one day, with every new account starting under the guardrails above. It replaced the inconsistent manual process with a documented multi-phase runbook.

How it works

  • SCPs never grant permissions. They cap what any identity in an account can be granted. Effective access is the overlap of the SCP and the identity's own policies.
  • Allows must exist at every level. For an action to be allowed, every SCP from the root down through each OU to the account must allow it. A deny anywhere wins.
  • SCPs don't apply to the management account, or to service-linked roles.
  • Delegated administration lets a dedicated security account run GuardDuty, Security Hub and Config for the whole organization, instead of the management account.

Things to watch

  • auto_enable_organization_members = "NEW" means new. Accounts that existed before you turned it on are not enrolled. They need an explicit enable ("ALL" covers both).
  • SCPs have a size limit (5,120 characters per policy). A long service list needs wildcards and careful structure, and sometimes more than one policy.
  • Roll out allow-lists carefully. Removing FullAWSAccess from an OU with workloads that use an unlisted service breaks them immediately. Check CloudTrail for which services each OU actually uses first. Sandbox accounts sit outside the allow-list, which gives teams somewhere to evaluate a service before it's approved for other accounts.
  • An allow-list needs a front door. Teams need a quick, clear way to request a new service, or they'll route around the platform.

What it solves

  • Every control change is a reviewed code change, with a history auditors can follow.
  • Unapproved services are denied by default across the organization.
  • No account starts ungoverned or unmonitored: guardrails and detection arrive with the account.
  • Provisioning time went from one week to one day.
  • One SIEM for both clouds: AWS findings and Azure firewall logs triaged together.

What it doesn't solve

  • Accounts that already existed need their own enrolment and review. Auto-enable only covers new members.
  • Misuse of approved services. An allow-list controls which services are used, not how. Policy-as-code, Control Tower detective controls and CSPM still matter.
  • The management account. SCPs don't restrict it, so it needs its own tight access controls and monitoring.
  • Detection without response. Findings in a SIEM only help if someone triages them. Response playbooks are a separate piece of work.

Checklist

  • [ ] SCP allow-list defined in code, replacing FullAWSAccess on governed OUs
  • [ ] Service usage checked in CloudTrail before each OU moves to the allow-list
  • [ ] A documented process for requesting new services
  • [ ] Control Tower controls defined as code, with deliberate OU exclusions
  • [ ] GuardDuty, Security Hub and Config under delegated administration
  • [ ] New accounts auto-enrolled; existing accounts enrolled explicitly
  • [ ] Findings forwarded to the SIEM, with someone responsible for triage
  • [ ] Account factory applies all of the above at creation

Further reading

Get in touch