Cloud Accounts, Identity, and Landing Zones
Consider a hypothetical team, called Orders here, that has finished building a service and needs somewhere to run it. The platform team could say “use the shared cloud account,” and for a month that would work. Then a second team arrives, a contractor needs read access to one database, a test script deletes a resource that production also uses, and nobody can say who is allowed to change the network. None of those problems comes from a bad tool. They come from having no place designed to receive workloads.
This post is about designing that place. By the end you should be able to explain what an account boundary does and does not isolate, set up federated sign-in for both people and automation without long-lived keys, reason about how several policy layers combine into one allow or deny decision, choose network and policy baselines that every new environment inherits, and decide by review and simple checks whether the result matches what you intended. The worked provider is AWS, because its documentation spells out the evaluation rules precisely. The concepts map to other clouds, and a short comparison with Azure appears in the first section. The post is about design and judgment, so the few fragments of policy text and arithmetic in it exist only to make a rule or a calculation concrete.
This post assumes the request-path, DNS, and routing material from Networking for Platform Engineers: DNS, TLS, Routing, and Timeouts. You will need to know what an IP range is, why two networks with overlapping ranges cannot be joined by simple routing, and how a name resolves to an address. The identity, permission, and policy material below is introduced from scratch.
What a landing zone is, and why it starts with accounts
A landing zone is a cloud environment prepared in advance for workloads: an account hierarchy, an identity model, network address space and connectivity, policy guardrails, and baseline logging, all deployed as configuration. The test of a landing zone is practical. Can a team be handed an environment that already satisfies the organization’s baseline, or are they handed a document describing one?
Providers package the idea differently. AWS Control Tower describes a landing zone as a multi-account environment built on security and compliance practices, and it orchestrates AWS Organizations, AWS Service Catalog, and AWS IAM Identity Center to build one. Microsoft’s Cloud Adoption Framework splits an Azure landing zone into a platform landing zone, which holds governance, security, and shared resources, and workload landing zones, which are the environments application teams operate inside the platform’s guardrails. In both cases the platform team owns the foundation and the workload teams own what runs on it. That is the same consumer and ownership split that applies to any platform service, here applied to the cloud environment itself.
The reason a landing zone starts with accounts is what an account is. The AWS whitepaper on organizing environments describes an account as an isolation boundary for identity and access management: when resources need to be shared across accounts, access must be explicitly allowed, and by default no access is allowed between accounts. The same document notes that the isolation helps with billing separation and with direct visibility of quotas and throttling, and that AWS charges depend on resource usage, not the number of accounts. Azure’s equivalent unit is the subscription, grouped under management groups.
That default is what makes an account a useful boundary for a platform engineer. An account is the largest unit that is denied by default, which makes it a natural container for blast radius: a compromised credential, a runaway script, or a mistaken deletion in one account is limited by what the account’s own identities and explicit cross-account grants can reach. It is a boundary, not a wall. Anything you deliberately connect, such as a shared network, a cross-account role, or a shared log bucket, is a path across it, and the rest of this post is largely about making those paths few, named, and checked.
Account boundaries
What the boundary separates
An account separates five things you will meet repeatedly.
- Identity and permissions. Roles, policies, and credentials created in one account do not apply in another unless a trust relationship is created.
- Resources. Quotas and limits are scoped to the account, so one workload exhausting a quota does not starve another.
- Cost attribution. Billing can be mapped to the account without tagging discipline across every resource.
- Policy attachment points. Organization-level policies attach to the root, to groups of accounts, or to single accounts, so the account is the smallest unit you can target.
- Audit scope. A question such as “who changed anything in the production database account last week” has a bounded answer.
It does not separate anything you connect. A network peering between two accounts’ networks, a role that trusts another account, and a bucket policy that grants access to an external principal all create reachability that the account boundary does not prevent. The AWS documentation on VPC peering is explicit that the accepter network can belong to another account, so peering is one way a boundary gets crossed on purpose.
Designing the hierarchy
AWS Organizations arranges accounts into organizational units (OUs) under a root. The hierarchy matters because policies inherit down it. The practical design question is therefore “which accounts need the same policy?” and not “what does our org chart look like?” Org charts change when teams reorganize; the policy needs of production accounts versus experimental accounts change much more slowly.
The following hierarchy is an illustrative design, not a prescribed one. The whitepaper treats its recommendations as guidance rather than a one-size-fits-all structure, and it advises keeping the security OU limited to the accounts that AWS Control Tower designates, with any additional security-related accounts in a separate OU.
Root
|-- Security (log archive account, audit account)
|-- Infrastructure (network account, shared tooling account)
|-- Workloads
| |-- Prod (one account per workload and environment)
| `-- NonProd
|-- Sandbox (individual experimentation, tighter guardrails and budgets)
`-- Suspended (accounts being closed; deny everything)
Three design choices in this sketch deserve a reason.
First, production and non-production accounts are separate OUs so policy can differ. For example, a rule that denies deleting a retention-locked log bucket might apply everywhere, while a rule that restricts which services may be used might apply only to Prod.
Second, one account per workload and environment keeps a failure or a permission mistake from crossing from, say, Orders production to Orders staging. The cost is more accounts to create, which is why automation is not optional: the whitepaper says managing multiple accounts often requires automation to limit operational complexity.
Third, shared concerns get their own accounts. Putting centralized logs in an account that workload teams cannot administer is what makes the logs trustworthy as evidence, a point developed in the audit section.
The management account and the root user
Two special identities need handling before anything else.
The organization’s management account is where the organization is administered. AWS states that service control policies (SCPs) do not affect users or roles in the management account. The practical consequence is that the management account is outside the guardrails you are about to build, so it should hold as little as possible: organization administration and little else, no workloads. This is a cleaner statement than “protect it carefully,” because the thing to avoid is putting a workload somewhere your own policies cannot constrain.
Each account also has a root user with complete access. AWS recommends not using the root user except for tasks that require it, requiring MFA, and not creating root access keys. For accounts in an organization, AWS describes two strategies: remove root credentials from member accounts and manage the rare root-only tasks centrally, or keep the credentials and protect them with MFA, optionally constrained by an SCP. Either way, monitor root sign-in. A detective control for root use is cheap to build, and a root sign-in in a workload account is rare enough that every occurrence should be explainable.
Vending accounts
The last piece of account design is how new accounts come to exist. If creating an account is a ticket and a two-week wait, teams will reuse an existing account, and the boundary you designed erodes. If creating one is a request that produces a baseline-compliant account in minutes, teams use the boundary.
AWS Control Tower’s Account Factory is one implementation: a configurable template that standardizes provisioning with pre-approved account configurations. The generalization, which also appears in the landing zone entry, is that a landing zone needs a repeatable request, creation, and handover process for application environments. The process can begin manually. The baseline must hold regardless of how the account was made, so the checks in the audit section run against all accounts, including ones made by hand.
Identity federation
Two kinds of caller
Everything that reaches the cloud API is either a person or a program. The AWS IAM best-practice guidance treats them separately but gives the same core instruction: require human users to use federation with an identity provider so they access AWS with temporary credentials, and require workloads to use temporary credentials by assuming IAM roles. The alternative, an IAM user with a long-lived access key, remains available for cases that cannot use federation, and the guidance recommends updating those keys when needed and requiring MFA when an IAM user or root user is unavoidable.
The security reason is simple. A long-lived key is a secret that never expires on its own; it lives in a laptop profile, a CI variable, or a repository, and anyone who obtains it is authenticated as its owner until someone notices. A temporary credential expires, and the means of obtaining it can be tied to something that is checked every time.
Workforce federation
For people, the pattern is: the organization’s identity provider (IdP) authenticates the person, including MFA, and the cloud trusts the IdP’s assertion. AWS IAM Identity Center is the AWS service built for this. It can connect an existing IdP, or hold users itself, and AWS describes it as one point of federation: you federate once and manage a single certificate when using a SAML 2.0 provider, and then manage which users and groups can reach which accounts.
IAM Identity Center has two instance types. The organization instance is deployed in the management account and is the only type that manages access to AWS accounts; AWS calls it the best practice for production use. Account instances are tied to a single account and meant for isolated deployments of certain managed applications. For account access, AWS describes permission sets as a way to create permissions for common job functions such as admin, provision them across accounts, and assign them to users and groups. Underneath, federated access to AWS accounts works by assuming roles that provide temporary credentials, which is the model the IAM guidance recommends for human users.
Notice what this does to the audit question “who can reach the production account?” The answer is a list of assignments in one place, instead of a search through every account’s users. It also changes offboarding: removing a person from the IdP removes the path, with no per-account keys to hunt for.
Federation moves a dependency to the IdP. If the IdP is unavailable, nobody signs in through it. AWS’s IAM guidance lists emergency access as a legitimate reason for IAM users: a small number of tightly controlled break-glass credentials, protected by MFA and monitored, that exist for the case where the identity provider cannot be reached. Test that path on a schedule, because an untested break-glass account tends to be broken when needed.
Workload federation with OIDC
Automation needs the same property: no stored secret, and a credential tied to something verifiable. OpenID Connect (OIDC) federation does this by letting a workload present a signed token from a trusted issuer in exchange for temporary cloud credentials. This is where the access token idea becomes concrete: the token is a JSON Web Token (JWT) carrying claims, and the cloud acts as the resource server that decides whether to accept it.
GitHub Actions to AWS is a widely used example, and GitHub’s documentation describes the flow. You register GitHub’s token issuer as an IAM OIDC identity provider. A workflow with the id-token: write permission can request a JWT from GitHub, and an action exchanges it for temporary AWS credentials. The IAM role’s trust policy decides which tokens may assume the role.
A trust policy is a resource-based policy attached to a role that states which principals are trusted to assume it. For OIDC federation the principal is the identity provider, the action is sts:AssumeRoleWithWebIdentity, and the conditions are where the security lives. GitHub’s documentation says the policy must check the audience (aud) claim equals sts.amazonaws.com, and recommends evaluating the subject (sub) claim to limit which repositories, branches, or environments can assume the role. The subject claim has several formats, such as one that names a repository and branch and another that names a repository and a GitHub environment. GitHub’s documentation also shows an alternative format using immutable identifiers for newer repositories, so the format you must match depends on the repository; the condition has to match the claims the issuer actually puts in the token, which GitHub’s documentation lists.
The AWS documentation on creating an OIDC provider adds the validation side: the provider’s discovery document must be reachable at a well-known path under the issuer URL, the token’s audience must match one registered on the provider, and AWS verifies the signature using the keys the issuer publishes. The inference from how trust policies evaluate conditions is that a role trusting the provider but not conditioning on sub accepts a valid token from any workflow that can obtain one from the issuer, including workflows in repositories you do not control. This is a serious mistake in this pattern, so the review table below covers it.
A correctly scoped trust policy looks like this. The account ID and repository are placeholders.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:example-org/orders:environment:prod"
}
}
}]
}
Binding the role to a GitHub environment named prod can add a second control: if the CI system lets you attach protection rules to that environment, those rules may gate which runs obtain a token whose sub names it. Whether such protections exist is a CI configuration question that this policy cannot verify, so the audit has to cover both sides.
Reviewing trust policies for this pattern comes down to three findings, and each one can be recognized by reading the policy text.
| Trust policy pattern | Finding |
|---|---|
aud equals sts.amazonaws.com and sub names one repository and environment | Scoped as intended |
aud is checked, sub uses a wildcard such as repo:example-org/* | Matches any repository, ref, and environment under that prefix |
aud is checked, no condition on sub | Under the inference above, any workflow that can obtain a token from the issuer may assume the role |
sub is checked, no condition on aud | The audience is unconstrained |
The wildcard case is a judgment call. A wildcard over an organization’s repositories may be acceptable for a sandbox account where any repository may deploy, and unacceptable for production. GitHub’s documentation says wildcards allow broader access and need careful security thought to keep untrusted repositories out. A review or a policy linter should make that decision visible, not make it.
Two limits of this mechanism are worth knowing before you design around it. Role chaining, which is using one role’s credentials to assume a second role, limits the session to one hour regardless of the roles’ configured maximum, and a DurationSeconds over one hour in a chained call fails. And temporary credentials bound the time window of reuse, not the damage done while they are valid; a stolen session token is usable until it expires.
Permission design
Layers that combine
Once a caller has an identity, a request is allowed or denied by several policy layers. Platform engineers get tripped up here more than anywhere else in cloud identity, because the layers do not all work the same way. The IAM documentation on policy evaluation gives the rules, which can be stated as follows for a request within one account in an organization.
- Identity-based and resource-based policies grant permissions. Within an account, the effective permissions from these two types are the union: an allow in either is enough, and an explicit deny in either overrides.
- Permissions boundaries limit what identity-based policies can grant. The result is the intersection of the boundary and the identity policy. A boundary grants nothing by itself.
- Service control policies set the maximum available permissions for IAM users and roles in member accounts. They grant nothing. The effective permissions are the intersection of what SCPs allow and what identity-based and resource-based policies allow.
- An explicit deny anywhere wins.
The service control policy entry covers SCP behavior in more depth, including exceptions where a resource-based grant interacts with boundaries. The points that matter most when designing a hierarchy are these, from the Organizations documentation:
- For an action to be allowed in an account, an explicit allow must exist at every level from the root through each OU on the path to the account, including the account itself. AWS attaches a managed policy named
FullAWSAccessat every level when SCPs are enabled, and warns that removing it without a replacement blocks everything below. - A deny at any level applies to everything beneath it, regardless of allows at lower levels.
- SCPs affect only IAM users and roles in member accounts, including a member account’s root user. They do not affect the management account, service-linked roles, or principals from outside the organization using a resource policy that grants them access.
- A user with no identity-based or resource-based allow has no access, even if SCPs allow everything.
A model of the decision order
Reading a list of rules is not the same as predicting a decision, so it helps to write the rules as a decision procedure and trace requests through it. The procedure below is a simplified model of the rules described above, not AWS’s implementation. It handles Allow and Deny, Action and NotAction, resource wildcards, SCP levels, a permissions boundary, and a same-account resource policy. It leaves out session policies, resource control policies, principal matching, and most condition operators, and it should not be used to make a real access decision.
The core of the procedure, in order, is:
def evaluate(action, resource, ctx, identity, scp_levels, boundary=None, resource_policy=None):
everything = identity + [p for lvl in scp_levels for p in lvl]
everything += [boundary] if boundary else []
everything += [resource_policy] if resource_policy else []
if explicit_deny(everything, action, resource, ctx):
return "DENY (explicit deny)"
for i, lvl in enumerate(scp_levels):
if not has_allow(lvl, action, resource, ctx):
return f"DENY (no SCP allow at level {i})"
if boundary and not has_allow([boundary], action, resource, ctx):
return "DENY (outside permissions boundary)"
if has_allow(identity, action, resource, ctx):
return "ALLOW (identity policy)"
if resource_policy and has_allow([resource_policy], action, resource, ctx):
return "ALLOW (resource policy)"
return "DENY (implicit)"
The function first looks for a matching explicit deny in any layer. It then requires an allow at every SCP level, then inside the boundary if there is one, and finally an allow from the identity policy or the resource policy. Anything left over is the implicit deny. The ten scenarios below are traced through it. They were also run through a small local implementation of the same procedure, with simple matching for actions, wildcards, and the one condition operator the region guardrail needs, and the results match the table. That run exercised the model on invented inputs, not AWS. The root-level SCPs are FullAWSAccess plus a region guardrail that denies everything except a few global services outside two approved regions.
| Scenario | Result under the procedure |
|---|---|
| Administrator identity, request in an approved region | ALLOW (identity policy) |
| Administrator identity, request in a region outside the guardrail | DENY (explicit deny) |
Administrator identity, iam:CreateRole from an unapproved region | ALLOW (identity policy) |
| No identity policy, SCPs allow everything | DENY (implicit) |
OU swaps FullAWSAccess for an S3-only allow, EC2 request | DENY (no SCP allow at level 1) |
| Same OU, S3 request | ALLOW (identity policy) |
Root has only the region deny and no FullAWSAccess | DENY (no SCP allow at level 0) |
Identity allows S3 read, boundary allows only EC2 and s3:GetObject, s3:ListBucket request | DENY (outside permissions boundary) |
Same, s3:GetObject request | ALLOW (identity policy) |
Resource policy allows s3:GetObject, identity has no policy | ALLOW (resource policy) |
Each row is a short lesson. The second shows that an administrator policy does not override a guardrail, which is the point of an SCP. The third shows why the region guardrail needs a NotAction list: global services are not tied to a region, and denying them by region would break administration. The AWS example this structure follows warns that some services that look regional handle requests globally and that the policy should be tested, so the exempt list needs validation against the services you use. The fourth shows that SCPs set a ceiling and never a floor. The fifth and seventh show the failure that locks teams out: an SCP set that lacks an allow at some level denies everything below it. The eighth shows that a boundary and an identity policy intersect, so an action an identity policy allows can still be denied by the boundary.
The tenth row relies on the same-account union described by the IAM documentation. It would not hold for every principal and resource-policy combination, which is why the entry on service control policies tells you to identify the principal type and the grant path when diagnosing a real denial. The model is deliberately simple; treat disagreement between it and the real service as a finding about the model.
Designing roles and delegation
With the layers understood, designing permissions becomes a set of decisions about who owns which layer.
SCPs belong to the platform team and express what must be true everywhere in an OU. Examples are preventing accounts from leaving the organization, denying actions outside approved regions, preventing changes to the audit trail, and restricting the root user. These are guardrails in the sense that no local administrator can grant past them. Write them as policy as code, review them like other infrastructure, and treat each change as a deployment: AWS recommends not attaching an untested SCP to the organization root, and suggests moving accounts into a test OU gradually and checking service last accessed data or CloudTrail to see which services accounts use.
Roles belong to the account’s owning team, within a boundary. The IAM best-practice guidance recommends permissions boundaries to delegate permissions management inside an account, for example letting developers create and manage roles for their workloads while capping what those roles can ever be granted. Without a boundary, the ability to create roles is the ability to create an administrator.
Start from job functions, then narrow with evidence. The guidance suggests beginning with AWS managed policies for common job functions and moving toward customer-managed policies specific to your use case, because managed policies are written for all customers and may not be least privilege for yours. Evidence for narrowing comes from IAM Access Analyzer, which can generate a policy from CloudTrail activity, validate policies with more than 100 checks, and report public and cross-account access, and from last accessed information that shows which permissions and credentials are unused. Treat generated policies as a draft: they reflect observed activity, which excludes the rare operation that has not yet run. Least privilege is a process of tightening with feedback, not a state you reach once.
Choose role-based or attribute-based rules deliberately. A role per environment and job function is easy to review but multiplies as combinations grow, while attribute-based rules, such as matching a tag on the principal to a tag on the resource, scale better and depend on the integrity of the attributes. The entry on role-based and attribute-based access control lays out how each fails quietly. In a landing zone, a mixed approach is common: coarse roles from the identity source and a few attribute conditions where roles would multiply.
Keep conflicting powers apart. The ability to change a pipeline, to approve its deployment, and to alter the role it deploys with are conflicting powers. Separation of duties means identifying which of these one identity must not hold together. For the OIDC deploy role above, one concrete rule is that the deploy role must not be able to modify its own trust policy or the SCPs above it. Whether that holds is a question about IAM permissions in the account, so it belongs on the audit list.
What SCPs cannot do
Guardrails are only as good as their coverage, so list the gaps. From AWS’s documentation, SCPs cannot restrict actions by the management account, actions that use a service-linked role’s permissions, and a small set of listed tasks. They affect principals in member accounts and do not directly affect resource-based policies, so a bucket policy that grants an external account access is governed by other controls such as resource control policies, which AWS documents as a separate organization policy type. SCPs also do nothing to data already shared or already copied. Where a control depends on total coverage, read the documented exceptions first instead of discovering them from an incident.
Network and policy baselines
Plan the address space first
Networks are the part of a landing zone that is hardest to change later, and the mistake that causes the most rework is overlapping address ranges. The reason is mechanical. Two networks that must exchange traffic need distinct address ranges so that a destination address identifies exactly one place. AWS’s VPC peering documentation states that you cannot create a peering connection between VPCs with matching or overlapping IPv4 or IPv6 CIDR blocks, and that if a VPC has several IPv4 blocks, an overlap in any of them blocks the peering even if you only wanted to use the non-overlapping ones. It also says peering is not transitive: if A peers with B and with C, B cannot reach C through A. Peering does not extend edge-to-edge routing either, so resources in one peer cannot use the other’s internet gateway, NAT device, VPN, or Direct Connect connection. The documentation points to transit gateways as an alternative for some of these cases.
Private address space comes from RFC 1918, which reserves 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 for private internets. A range outside those blocks changes DNS behavior in peering: the documentation notes that for a VPC outside the RFC 1918 ranges, private DNS hostnames cannot be resolved to private addresses unless DNS resolution support is enabled for the peering connection. Name resolution across networks is therefore part of the address plan, not an afterthought; this is the DNS material from the prerequisite post meeting accounts.
An allocation plan is a table: for each account and environment, a range, an owner, and a note about whether it will be connected to others or to an on-premises network. A check for overlap needs nothing more than the Python standard library. The two ranges in the snippet are placeholders chosen so that the second sits inside the first.
import ipaddress
orders_prod = ipaddress.ip_network("10.0.32.0/20")
payments_prod = ipaddress.ip_network("10.0.40.0/21")
print(orders_prod.overlaps(payments_prod))
print(orders_prod.num_addresses, payments_prod.num_addresses)
print(payments_prod.subnet_of(orders_prod))
Running the snippet prints three lines: True, then 4096 2048, then True. The first line is the check you would run over every pair of planned ranges in review, the second shows the address counts (a /20 has 4,096 addresses before the provider reserves any, a /21 has 2,048), and the third shows that here the second range is entirely inside the first, which is why no routing configuration could separate them. By hand, a /20 spans 16 values of the third octet, so starting at 32 it ends at 47, and a /21 spans 8, so starting at 40 it also ends at 47.
Detecting the overlap before any network exists is the cost advantage of putting the plan under version control and running such a pairwise check in review. The sizing choice is a trade-off: large blocks waste a finite private space and small blocks force renumbering when a workload grows. Pick an allocation unit that leaves headroom, and keep a record of what is allocated, not only what is used.
Who owns the network is a separate decision. One pattern gives each workload account its own network and connects them through peering or a transit hub; another places subnets in a central account and shares them with workload accounts. The entry on Shared VPC describes one provider’s version of the second pattern and what changes about ownership: workload teams are granted a place to put things instead of owning a network and negotiating connections. The trade-off is that a change to the shared network affects every workload using it, so the central owner needs change review proportionate to that radius. Neither pattern is correct in general; the right choice depends on how many teams you serve, how much network skill they have, and how much central control you need.
Policy baseline
A policy baseline is the set of rules a new account inherits without anyone configuring them. A modest starting set, each of which maps to a documented control:
- Deny leaving the organization, so an account cannot escape the hierarchy’s policies.
- Deny API calls outside approved regions, with the exemption list for global services.
- Protect the audit trail by denying actions that stop, delete, or reconfigure the organization’s logging.
- Restrict the root user in member accounts, if root credentials are kept.
The Terraform below expresses the second item and attaches it to a sandbox OU. terraform fmt -check was run on the file and reported no formatting differences, which shows only that the file is well-formed HCL. terraform validate, plan, and apply were not run, because those need the AWS provider and an AWS account, so the resource arguments were not verified against the provider. The AWS provider’s argument documentation for the OU, policy, and OIDC provider resources was read to choose the arguments. Treat it as a sketch of the structure, to be validated before use.
resource "aws_organizations_policy" "region_guardrail" {
name = "deny-unapproved-regions"
type = "SERVICE_CONTROL_POLICY"
content = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Deny"
NotAction = ["iam:*", "cloudfront:*", "route53:*", "support:*"]
Resource = "*"
Condition = {
StringNotEquals = {
"aws:RequestedRegion" = ["eu-central-1", "eu-west-1"]
}
}
}]
})
}
# Attach to an OU first, not the root, so a mistake affects a small set of accounts.
resource "aws_organizations_policy_attachment" "sandbox_region" {
policy_id = aws_organizations_policy.region_guardrail.id
target_id = aws_organizations_organizational_unit.sandbox.id
}
The comment carries the rollout rule: attach to a small OU, observe, then widen. Region choice is yours; the two regions here are arbitrary. A region guardrail also has a cost: it blocks use of any service whose control plane or required resources live elsewhere, so it needs an exception process, an owner for the approved list, and a place to see denials.
Detective baseline
Preventive rules reduce what can happen; detective controls record what did. AWS CloudTrail supports an organization trail that logs events for the management account and all member accounts. Member accounts can see the trail, but users in them do not have permission to delete it, stop logging, or change what it logs, and by default only the management account has access to the S3 bucket holding the logs. The documentation also notes that when an account leaves the organization, its earlier logs remain in the bucket.
That protection is what makes the logs usable as evidence: an actor with administrative rights in a workload account cannot erase the record of what they did, which is the conflict named under separation of duties. An audit log is valuable in proportion to its integrity and its coverage, so deliver it to an account that workload administrators cannot administer, and add a preventive rule that denies stopping or deleting the trail. A trail that is not read helps nothing, so decide who reviews which events and how an alert reaches them. AWS’s root-user guidance suggests using the existing monitoring stack to alert on root sign-in and usage, and AWS Config managed rules exist to check root MFA and root access keys.
Implementation and audit
An order that works
Landing zones are built in a sequence, because each step depends on the one before. The order below is a recommendation, not a requirement from any one document.
- Secure the management account and its root user. Require MFA, avoid access keys, and use a group-managed email address for root credentials, as AWS recommends.
- Create the organization and the top-level OUs. Keep the hierarchy small; moving accounts later is easier than untangling an over-built tree.
- Connect the identity source and assign access. Use the organization instance of IAM Identity Center or your provider’s equivalent, and create permission sets for a few job functions.
- Stand up the security accounts and the organization trail. Logging before workloads means the first workload is already recorded.
- Attach baseline guardrails to a sandbox OU first. Observe denials, fix what breaks, and then widen scope.
- Publish the address plan and create the network account. Allocate ranges before creating networks.
- Build account vending. A manual request is a legitimate first version, as long as it yields a baseline-compliant account.
- Hand the first real workload an account, and learn from it. The landing zone programs described in the landing zone entry fail by spending a long time on a complete catalog before any workload arrives; start with the minimum above and extend it from observed need.
Everything in this list should end up in version control and be applied by a pipeline. Infrastructure as code is how the baseline stays reviewable and reproducible, and it brings its own hazards: the state that tracks what the code created, concurrent changes, drift, and secrets in plans. Those are the subject of the next post, Infrastructure as Code: State, Drift, and Safe Changes. One bootstrapping problem is specific to landing zones and is worth naming now: the code that creates the organization needs a place to keep its own state and credentials before the organization exists. A small, manually created, tightly controlled bootstrap account or the management account itself is the usual starting point, with the decision recorded and the access to it treated as highly privileged.
Audit questions and where the evidence comes from
An audit asks whether the landing zone’s actual configuration matches its intended design. The useful form is a list of questions, each with a source of evidence and a named reviewer. The table gives a starting set; none of the checks is a complete assurance.
| Question | Evidence source | What it cannot show |
|---|---|---|
| Who can reach the production account, and with which permission set? | Identity Center assignments for the account | Access that bypasses Identity Center, such as IAM users or roles with their own trust policies |
| Which roles can be assumed by a principal outside the account or organization? | IAM Access Analyzer findings for public and cross-account access, review of trust policies | Access that has never been exercised but is still granted; whether the external party is trustworthy |
| Which trust policies accept OIDC tokens without a subject condition? | Review or lint of trust policies (see the patterns above) | Whether the CI system’s environment protections are configured |
| Are any permissions or credentials unused? | IAM last accessed information | Permissions needed rarely, such as disaster recovery actions |
| Do any two networks overlap? | The address plan and a pairwise overlap check like the one above; the actual VPC CIDR blocks | Overlap with a partner or on-premises range missing from the plan |
| Is the organization trail on, and can workload administrators change it? | Trail status, SCP denying changes, bucket policy | Events the trail does not capture, for example data events not enabled |
| Is root used? | Root sign-in alerts and CloudTrail sign-in events | Use that predates monitoring |
| Does an SCP change do what the change author expects? | A test in a sandbox OU, with denied calls reviewed | Interactions with SCPs and policies added later |
Two habits make the table useful. First, test denials as well as grants: a guardrail is working only if the forbidden action fails for a principal that would otherwise be allowed, such as an administrator in the sandbox account. This needs a real account, and it was not run. Second, run the checks on a schedule, not only at design time. Accounts and roles are created continuously, so the answer to “does production still match the design” has a date on it. When a check finds a difference, deciding whether the code or the live account is wrong follows the configuration drift discussion: correct the declaration if it is wrong, restore the account if an unauthorized change caused the difference, and record an authorized emergency change with an owner, reason, and expiry.
How it fails
The failures below follow from the mechanisms above; each can be tested before it happens.
| Failure | Mechanism | Detection or prevention |
|---|---|---|
| Lockout from a new SCP | A level lacks a needed allow, or a deny is broader than intended | Attach to a test OU first; keep a tested recovery path for administering policies |
| Guardrail gap through the management account | SCPs do not apply to it | Keep workloads out; monitor its use; protect its few identities strongly |
| OIDC role assumed by an unintended repository | Missing or wildcard sub condition | Static policy check; review wildcards against the account’s sensitivity |
| Federation outage | IdP or Identity Center unavailable | Tested break-glass access with MFA and alerting on use |
| Late discovery of overlapping address ranges | Plan not enforced before network creation | Check the plan in review; reserve ranges centrally |
| Permission boundary missing on delegated role creation | Delegates can create roles broader than their own | Require a boundary on any role-creating permission |
| Account created outside the process | Vending is slower than going around it | Make the sanctioned path fast; audit for accounts that lack the baseline |
| Logs altered or lost | Same actors administer workload and log storage | Separate log account, deny changes to the trail, review who can read logs |
Several limits remain. A landing zone reduces the chance and the radius of mistakes; it does not make a workload secure, and an overbroad role inside an account is still overbroad inside it. The documentation relied on here describes AWS behavior at the time of writing, and policy-evaluation details, available controls, and defaults change, so confirm the details in the current documentation before applying them. And nothing cloud-facing in this post was executed against a real account: the decision procedure and the address check were run locally on invented inputs, the Terraform was only syntax-checked, and everything else is documented behavior.
References
- AWS Whitepaper: Organizing Your AWS Environment Using Multiple Accounts
- AWS Control Tower User Guide: What is AWS Control Tower?
- Microsoft Learn: What is an Azure landing zone?
- AWS IAM User Guide: Security best practices in IAM
- AWS IAM User Guide: Root user best practices for your AWS account
- AWS IAM User Guide: IAM roles
- AWS IAM User Guide: Create an OpenID Connect (OIDC) identity provider in IAM
- AWS IAM User Guide: Policy evaluation logic
- AWS IAM Identity Center User Guide: What is IAM Identity Center?
- AWS Organizations User Guide: Service control policies (SCPs)
- AWS Organizations User Guide: SCP evaluation
- AWS Organizations User Guide: Example service control policies
- AWS CloudTrail User Guide: Creating a trail for an organization
- Amazon VPC Peering Guide: How VPC peering connections work
- GitHub Docs: Configuring OpenID Connect in Amazon Web Services
- IETF RFC 1918: Address Allocation for Private Internets
- Terraform AWS provider documentation source: aws_iam_openid_connect_provider, aws_organizations_organizational_unit, aws_organizations_policy
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
