Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/content/blog/aws-resource-control-policies-rcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "AWS Resource Control Policies (RCPs) vs SCPs"
description: "AWS resource control policies are Organizations resource-based guardrails: they cap what any caller—including principals outside the org—can do to member-account resources. SCP vs RCP vs IAM, a public-S3 deny, sandbox OU tests."
pubDate: 2026-08-27
updatedDate: 2026-08-27
updatedDate: 2026-10-02
author: OpenSourceOM Team
tags:
- AWS
Expand Down Expand Up @@ -61,7 +61,7 @@ They **do not** apply to:
- **AWS managed KMS keys**
- `kms:RetireGrant` (special-cased)

When you enable RCPs, AWS attaches `RCPFullAWSAccess` (allow all through the RCP layer) to root, OUs, and accounts. Removing that without a replacement is how you freeze S3.
When you enable RCPs, AWS attaches `RCPFullAWSAccess` (allow all through the RCP layer) to root, OUs, and accounts, and does not let you detach it. A custom RCP Allow therefore does not narrow the account. Narrowing is a Deny, and that Deny is decided before any Allow — [SCP vs RCP evaluation order](/blog/aws-scp-rcp-evaluation-order/).

## SCP vs RCP vs IAM

Expand Down Expand Up @@ -130,7 +130,7 @@ Remaining public-or-cross-account edges after RCP belong on [attack path analysi

## Checklist

- [ ] Policy type `RESOURCE_CONTROL_POLICY` enabled; `RCPFullAWSAccess` left in place
- [ ] Policy type `RESOURCE_CONTROL_POLICY` enabled; custom statements are Deny, because `RCPFullAWSAccess` cannot be detached
- [ ] First Deny attached to a sandbox OU/account, not the org root
- [ ] External-account GetObject test **fails**; in-org app role GetObject **succeeds**
- [ ] `aws:PrincipalIsAWSService` exception verified with Trail/Config
Expand Down
117 changes: 117 additions & 0 deletions src/content/blog/aws-scp-rcp-evaluation-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: "SCP vs RCP Evaluation Order"
description: "AWS checks every explicit Deny before RCP and SCP Allows. RCPFullAWSAccess cannot be detached, so an RCP narrows access only with Deny — an identity Allow never skips that."
pubDate: 2026-10-02
updatedDate: 2026-10-02
author: OpenSourceOM Team
tags:
- AWS
- Organizations
- SCP
- RCP
- IAM
focusKeyword: SCP RCP evaluation order
faq:
- question: Does AWS evaluate SCPs before RCPs?
answer: >-
No. The enforcement code first looks for an explicit Deny in every
applicable policy, including both SCPs and RCPs. Only if none match
does it require an Allow in RCPs, then an Allow in SCPs, then
identity and resource policies. An Allow in an SCP does not skip
an RCP Deny.
- question: Can an RCP Allow replace RCPFullAWSAccess and narrow the account?
answer: >-
No. When you enable RCPs, AWS attaches RCPFullAWSAccess and does not
let you detach it, so the RCP Allow step always succeeds. A second
RCP that only Allows s3:GetObject does not remove the pass-through
Allow. Narrowing is a Deny statement, and that Deny is decided in
the first step.
- question: Why does a bucket-account SCP fail to stop another account?
answer: >-
SCPs apply to principals in the account where the SCP is attached.
A caller in a different account is not that principal. The bucket
account's RCP still applies, because RCPs apply to the resource.
A bucket policy Principal of that other account does not skip the
RCP Deny.
---

An identity policy Allows `s3:GetObject`. The bucket policy Allows the same role. SCP `FullAWSAccess` is attached. The call still returns `AccessDenied`, and the CloudTrail `errorMessage` mentions an Organizations policy. The statement that matched is an RCP `Deny`. It was decided **before** any of those Allows were consulted.

**SCP vs RCP evaluation order** is that sequence. Which policy type attaches to principals vs resources is [AWS resource control policies](/blog/aws-resource-control-policies-rcp/). This page is only the order the enforcement code walks. Official sequence: [How AWS evaluates requests to allow or deny](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic_policy-eval-denyallow.html).

## The sequence

For a request evaluated in an account that is an Organizations member:

1. **Explicit Deny.** Every applicable policy is scanned for a `Deny` that matches the request: SCPs, RCPs, identity-based policies, resource-based policies, permissions boundaries, session policies. One match is a final Deny. Evaluation stops.
2. **RCP Allow.** If RCPs are enabled, some RCP statement must Allow the action. `RCPFullAWSAccess` is attached to the root, every OU, and every account, and AWS does not let you detach it, so this step finds an Allow. A custom RCP Allow is redundant with that pass-through.
3. **SCP Allow.** Some SCP statement must Allow the action. `FullAWSAccess` is the usual pass-through. Unlike `RCPFullAWSAccess`, the SCP managed policy **can** be detached. If nothing else Allows, this step is a final Deny.
4. **Resource-based policy, then identity-based policy.** In the same account, an Allow in either is enough for most services. IAM role trust policies and KMS key policies still need their own Allow. An implicit deny in the identity policy does not cancel a resource-policy Allow granted directly to the IAM user or to the role-session ARN.
5. **Permissions boundary.** When the Allow came from an identity policy, the boundary must Allow the action too. A resource-policy Allow granted directly to the IAM user or the role-session ARN is not limited by a missing Allow in the boundary. An explicit Deny in the boundary already finished the request in step 1.
6. **Session policy.** If the caller passed a session policy and it does not Allow the action, final Deny. No session policy means this step Allows. The same direct resource-policy grant to a role-session ARN is not limited by an implicit deny in the session policy.

```
request
→ any explicit Deny? stop, Deny
→ RCP Allow present? else Deny (RCPFullAWSAccess always is)
→ SCP Allow present? else Deny
→ identity or resource Allow
→ boundary Allow
→ session Allow
→ Allow
```

There is no “closest policy wins,” and a child OU Allow does not outrank a parent Deny. The parent Deny already matched in step 1.

## What an RCP can actually change

Because step 2 always sees `RCPFullAWSAccess`, an RCP changes the decision only by matching a **Deny in step 1**.

| What you attach | What the order does with it |
| --- | --- |
| RCP `Deny` on `s3:*` unless `aws:PrincipalOrgID` matches | Step 1. Final Deny for callers the condition catches. Later Allows are not read. |
| RCP `Allow` of only `s3:GetObject` | Step 2 still passes via `RCPFullAWSAccess`. Other S3 actions stay allowed if IAM allows them. |
| SCP `Deny` of `iam:CreateUser` | Step 1. Final Deny. An AdministratorAccess identity policy is not consulted. |
| SCP `Allow` of only `ec2:*` and `s3:*`, with `FullAWSAccess` detached | Step 3. Actions outside that Allow are a final Deny. |
| Identity `Allow` of `s3:*` | Reached only if steps 1–3 did not already Deny. |

A sandbox test that “proves” the RCP Allow list works is usually proving that `RCPFullAWSAccess` is still attached. Detach is not available. The test that matters is an explicit Deny from a principal the condition should catch, and a success from a principal it should not.

The condition operators on that Deny are evaluated inside step 1, not after IAM. `StringNotEqualsIfExists` does not match when the key is absent, so that Deny statement does not apply and evaluation continues. That is a different bug from order; the statement shape is in the [RCP vs SCP](/blog/aws-resource-control-policies-rcp/) page. A Deny that **does** match is never undone by an Allow later in the list.

## External callers

SCPs apply to **principals in the account where the SCP is attached**. RCPs apply to **resources in the account where the RCP is attached**.

A caller in account B, reading a bucket in account A:

- Account A’s SCPs do not apply to B’s principal. An SCP Deny in A is invisible to this request.
- Account A’s RCPs do apply. A matching RCP Deny is step 1 in A and is final.
- Account A’s bucket policy is the resource-based policy. Its Allow does not skip the RCP Deny.
- Account B still evaluates B’s own SCPs and B’s identity policy on the way out. B’s SCP Allow does not skip A’s RCP Deny.

Same-account evaluation and cross-account evaluation are different flows. The cross-account diagram is [Cross-account policy evaluation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic-cross-account.html). The practical rule on this page: a resource policy `Principal` of another account is not evidence that the resource account’s SCP ran, and it is not evidence that the RCP did not.

Service-linked roles skip SCPs and RCPs. A Deny that “should” have caught CloudTrail and did not is often that skip, not a reordering. The service-principal condition belongs on the Deny statement itself.

## Two tests

Run both in a sandbox OU before the Deny moves anywhere else. There is no RCP audit mode.

1. **In-org role that IAM allows.** `s3:GetObject` succeeds. If it fails, the Deny condition is matching principals you meant to keep (org id typo, missing service-principal exception).
2. **Principal outside the org** whose bucket policy Allows them. `s3:GetObject` is `AccessDenied`. If it succeeds, the RCP Deny did not match. The bucket account’s SCP was never going to be the control that failed.

CloudTrail in the bucket account shows the denied call. The identity policy in the caller account will still look correct. That is what step 1 looks like from the outside.

Failure mode: debugging for a day inside the role’s permission boundary because the boundary is step 5 and the RCP already finished at step 1. Read `errorMessage` for the Organizations policy id before editing IAM.

## Checklist

- [ ] RCP statements that are meant to constrain are `Deny`. An RCP `Allow` list is not a ceiling while `RCPFullAWSAccess` is attached
- [ ] SCP `FullAWSAccess` detached only when another SCP Allow covers every action you intend to keep
- [ ] External-account `GetObject` test fails; in-org app role `GetObject` succeeds
- [ ] Parent OU Deny tested from a child account. A child Allow did not change the result
- [ ] Service-linked roles and `aws:PrincipalIsAWSService` checked against CloudTrail delivery, not assumed from the order
- [ ] Access Analyzer or the IAM policy simulator used with the Organizations policies included. A simulator run that omits RCPs will show Allow

**Related:** [AWS resource control policies](/blog/aws-resource-control-policies-rcp/) · [AWS security best practices](/blog/aws-security-best-practices-2026/) · [Attack path analysis](/blog/attack-path-analysis-cloud-security/)
106 changes: 106 additions & 0 deletions src/content/blog/azure-blueprint-locks-deny-assignments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: "Azure Blueprint Locks Are Deny Assignments"
description: "Blueprint Read Only and Do Not Delete locks are deny assignments. Microsoft removes those denies on 31 January 2027. Map them onto a deployment stack before then."
pubDate: 2026-10-02
updatedDate: 2026-10-02
author: OpenSourceOM Team
tags:
- Azure
- Blueprints
- deny assignments
- deployment stacks
- landing zone
focusKeyword: Azure Blueprint lock deny assignment
faq:
- question: What happens to blueprint locks on 31 January 2027?
answer: >-
Azure Blueprints is retired that day. The API stops, and blueprint
locks — the deny assignments — are removed. Resources the blueprint
deployed stay in place and keep ordinary RBAC. Subscription Owners
can then delete or edit what the lock was blocking. Policy assignments
are a different object and are not removed by this retirement.
- question: Which deployment stack deny setting replaces Read Only?
answer: >-
denyWriteAndDelete. Do Not Delete maps to denyDelete. A template spec
stores the template and creates no deny assignment. The lock exists
only if you deploy that template with a stack and set deny settings.
- question: Can I delete the blueprint deny assignment from IAM?
answer: >-
Not while the blueprint assignment still owns it. Change the lock
mode or remove the assignment after a deployment stack is already
enforcing the same deny. Deleting the blueprint first drops the
protection for the whole window until the stack exists.
---

The hub firewall’s IAM blade shows a deny assignment the platform team did not create with `az role assignment create`. The name and description point at a blueprint assignment, lock mode `AllResourcesDoNotDelete`. That deny is the lock. It is not an Azure Policy `Deny`, and it is not on the role-assignment list.

**Azure Blueprint locks** are deny assignments created by the blueprint assignment. How deny assignments differ from Policy, and how landing zones use them on purpose, is [landing-zone deny assignments](/blog/azure-landing-zone-deny-assignments/). This page is the blueprint lock itself, and the date Microsoft removes it.

Retirement schedule: [Azure Blueprints retirement](https://learn.microsoft.com/en-us/azure/governance/blueprints/blueprint-retirement). Lock behavior: [resource locking](https://learn.microsoft.com/en-us/azure/governance/blueprints/concepts/resource-locking).

## What is already on the clock

| Date | What changes |
| --- | --- |
| 31 July 2026 | New blueprint definitions and versions can no longer be created. This date has passed. |
| 31 October 2026 | Existing definitions can no longer be modified. New blueprint assignments can no longer be created. |
| 31 December 2026 | Existing assignments can no longer be modified. |
| 31 January 2027 | The API stops. Definitions and assignments disappear from the portal. **Blueprint deny assignments are removed.** Deployed resources remain. |

A subscription Owner who cannot delete the hub today can delete it on 1 February 2027 if the only control was the blueprint lock. Policy assignments the blueprint also deployed are separate resources; this retirement does not delete them. Anything you were enforcing only with the lock — not with Policy, not with a management lock, not with a deployment stack — goes away that day even if you never press delete.

Export definitions you still need before 31 January 2027. After retirement they are not recoverable from the service.

## Lock mode to deny setting

| Blueprint lock | What the deny assignment blocks | Deployment stack `--deny-settings-mode` |
| --- | --- | --- |
| None | Nothing from the blueprint | `none` |
| Do Not Delete (`AllResourcesDoNotDelete`) | Delete | `denyDelete` |
| Read Only (`AllResourcesReadOnly`) | Write and delete | `denyWriteAndDelete` |

The blueprint assignment’s identity is excluded so the blueprint can still update its own resources. Everyone else, including Owner, hits `AuthorizationFailed`. `az role assignment list` does not show this object. List deny assignments:

```bash
az rest --method GET \
--url "https://management.azure.com/subscriptions/${SUB}/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01"
```

Match `properties.description` / `denyAssignmentName` to the blueprint assignment. Portal: Subscription → Access control (IAM) → Deny assignments. “Created by” is the tell. A `CanNotDelete` resource lock is a different blade and survives blueprint retirement; do not confuse the two when you inventory what is actually protecting the hub.

A template spec (`Microsoft.Resources/templateSpecs`) stores a versioned template. It does not create a deny assignment. Moving the blueprint JSON into a template spec and stopping there drops the lock on the day you remove the blueprint, not on 31 January 2027.

## Replace the lock before you remove the blueprint

Put the stack at the **parent** of the resources — management group for a subscription-scoped set, subscription for a resource-group set — so the people who have Owner on the workload cannot delete the stack and take the deny with it. Microsoft’s migration path is deployment stacks, not a second blueprint.

```bash
az stack sub create \
--name platform-hub \
--location eastus \
--subscription "${SUB}" \
--template-file hub.bicep \
--action-on-unmanage detachAll \
--deny-settings-mode denyDelete \
--deny-settings-excluded-principals "${PLATFORM_GROUP_OBJECT_ID}"
```

`denyDelete` is the Do Not Delete equivalent. Use `denyWriteAndDelete` for Read Only. Excluded principals are Entra object ids, at most five; a platform group is the usual one, because removing a person should be a group edit rather than a stack update. `--action-on-unmanage detachAll` means deleting the stack later detaches the resources instead of deleting them. `deleteAll` deletes the managed resources. On a hub, that flag is the outage.

Confirm the stack’s deny assignment is listed and that a non-excluded Owner still cannot delete a protected resource. Then remove the blueprint assignment. The other order — delete the blueprint, then author the stack — is a window where Owner works. Do that in a sandbox subscription, not on the hub.

Deployment stacks and who is allowed to edit `denySettings` are easy to widen: subscription Owner on the stack’s scope can change the mode. Parent scope is the control. Break-glass for the stack is the same problem as any other deny assignment: an excluded principal you can still operate, tested before you need it. The landing-zone page covers that debug loop.

Failure mode: two denies on the same firewall, blueprint and stack, and an operator deletes the blueprint assignment thinking it is unused. The stack deny should remain. Check the deny list after the delete and confirm one assignment is still there. Failure mode: `deny-settings-excluded-principals` is a user who has left, stack updates start failing, and someone sets the mode to `none` to unblock CI. That is the lock being removed on purpose.

## Checklist

- [ ] Every production blueprint assignment’s lock mode written down (None vs Do Not Delete vs Read Only)
- [ ] Deny assignments correlated to those assignments; resource locks and Policy Deny counted separately
- [ ] Replacement stack exists at parent scope with `denyDelete` or `denyWriteAndDelete` **before** the blueprint assignment is removed
- [ ] `--action-on-unmanage detachAll` on stacks that must not delete the hub
- [ ] Excluded principal is a platform group object id, and a non-member Owner still cannot delete
- [ ] Definitions exported before 31 January 2027
- [ ] A calendar reminder that 31 October 2026 stops new assignments and definition edits, and 31 January 2027 removes the denies whether or not you migrated

**Related:** [Landing-zone deny assignments](/blog/azure-landing-zone-deny-assignments/) · [Azure CSPM implementation](/blog/azure-cspm-implementation-guide/) · [CIEM explained](/blog/ciem-explained-for-cloud-teams/)
Loading
Loading