Guardrails are what stay true when people make mistakes
IAM answers the question “what is this person allowed to do?” Guardrails answer a different one: “what is nobody allowed to do here, whatever their role?” In AWS, that second job belongs to service control policies. In Azure, it belongs to Azure Policy applied through management groups.
The distinction matters because people with broad permissions are exactly the people most likely to cause a large mistake or to have their credentials stolen. A guardrail still holds in both cases. This page explains how we design and roll them out as part of a landing zone, on top of the account structure.
What an SCP is, and what it is not
An SCP does not give anyone permission. It filters. The effective permissions in an account are the intersection of what the SCPs allow and what IAM grants. A deny in an SCP wins over everything. Two consequences follow:
- A new organisation starts with an allow-all policy attached (FullAWSAccess). You build protection by adding denies, not by removing that default, unless you deliberately adopt an allow-list model.
- SCPs never apply to the management account, and they do not affect service-linked roles.
The hierarchy
Policies inherit down the tree, so the tree decides how much policy work you do. A shallow structure such as Security, Infrastructure, Workloads (production and non-production) and Sandbox lets you attach the universal rules at the root, stricter ones to production, and looser ones to the sandbox. Deep trees become hard to explain. On Azure, a similar tree of management groups sits above subscriptions, and policy assignments flow the same way.
A starting set of guardrails
The following are common to nearly every estate we build. Each should be adapted to the services you use.
- Protect the audit trail: deny stopping, deleting or modifying CloudTrail trails and Config recorders created by the baseline.
- Constrain regions: deny actions outside the regions you operate in, with exceptions for global services.
- Protect the baseline: deny changes to the roles and stacks the landing zone installs.
- No leaving the organisation: deny
organizations:LeaveOrganization. - No IAM users with keys: deny creating access keys for users, pushing everyone to roles and SSO.
- Block root use in member accounts: deny all actions when the principal is root.
An example: region restriction
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyOutsideApprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"route53:*",
"cloudfront:*",
"support:*",
"sts:*",
"budgets:*",
"ce:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["eu-west-1", "us-east-1"]
}
}
}
]
}
The list of exempt global services is the part teams get wrong. If it is too short, ordinary operations fail; too long, and the guardrail is porous. We build it from CloudTrail data and test, which is why the sandbox stage exists. Control Tower offers a managed region deny control that handles much of this for you, which is one reason we often start there.
Managing them in Terraform
resource "aws_organizations_policy" "deny_regions" {
name = "deny-unapproved-regions"
type = "SERVICE_CONTROL_POLICY"
content = file("${path.module}/policies/deny-regions.json")
}
resource "aws_organizations_policy_attachment" "workloads" {
policy_id = aws_organizations_policy.deny_regions.id
target_id = aws_organizations_organizational_unit.workloads.id
}
Policies live in version control and change through reviewed pull requests, using the pipeline approach described on our Terraform code page.
Detective controls complement preventive ones
SCPs stop things; they do not tell you what almost happened. Pair them with AWS Config rules and Security Hub findings, set up in the security baseline, so that denied calls and configuration drift are visible. A sudden run of AccessDenied events from one principal is a useful signal.
Azure Policy in practice
Azure Policy definitions with a Deny effect block creation of non-compliant resources, for example storage accounts allowing public blob access, resources in unapproved locations or virtual machines without required tags. Audit and DeployIfNotExists effects detect and remediate rather than block. A typical design uses initiatives (groups of policies) assigned at management groups, and starts in audit mode to learn the impact before switching to deny.
Decisions and trade-offs
Deny list or allow list? Allow-list SCPs are tighter but need constant maintenance as new services appear. Most teams are better served by a deny list covering the dangerous actions.
How many policies? SCPs have size and attachment limits, and each extra one is something to understand. Fewer, clearer policies win.
Tag enforcement. Requiring tags at creation helps cost allocation, but blocks automation that is not tag-aware. We often enforce tags with tag policies and Config rather than SCP denies.
When not to add one. If a rule would prevent legitimate work more often than it prevents harm, handle it with monitoring and review instead.
Rolling a new guardrail out safely
The order of operations is what separates a guardrail from an outage. We follow the same sequence each time. First, we search CloudTrail for the actions the new rule would deny, over a representative period, so we know in advance what it will affect. Second, we attach it to the sandbox OU and deliberately attempt the forbidden action, confirming that it fails with the expected message. Third, we run a normal deployment and a normal day of developer activity in a non-production account to confirm nothing legitimate breaks. Only then does it reach production, ideally at a quiet moment with someone watching denied-call events. If a problem appears, detaching the policy is a one-line change that takes effect quickly, and the rollback is rehearsed rather than improvised.
Common failure modes
- Locking yourself out. An overly broad deny blocks the very role you need to fix it. We keep a tested break-glass role excluded by condition.
- Untested region lists. Global service calls routed to us-east-1 start failing.
- Policy drift. Someone edits a policy in the console. We manage them as code and detect changes.
- Exceptions that never expire. We record each with an owner and a review date.
Delivery and handover
We deliver the OU or management group tree, policies in code, a table describing each rule’s purpose and a procedure for requesting an exception. Your team learns how to test a change in the sandbox OU. If you want regular review of guardrails alongside the rest of your platform, that fits naturally under our Managed DevOps service.
How to tell it is working
- A deliberate attempt to stop CloudTrail in a workload account fails.
- Resources cannot be created in unapproved regions.
- Denied-call events are reviewed rather than ignored.
- Every exception has an owner and an expiry.
To design guardrails for your own organisation, contact us.