The assumption
The first time I opened Alibaba Cloud's RAM console after spending years writing Kubernetes RBAC manifests, the vocabulary felt reassuringly familiar. Roles. Policies. Principals. Attachments. It was tempting to treat RAM as "RBAC, but for the whole account" and carry every intuition I had about Kubernetes permissions straight over.
That transfer works for about the first ten minutes. Then it starts producing wrong answers.
The key idea
RBAC binds a role to an identity inside one cluster's boundary. RAM binds a policy to an identity across an entire account's surface area. Treating them as interchangeable concepts understates what a RAM mistake can reach.
The problem
Kubernetes RBAC has a hard boundary: a Role is scoped to a namespace, a ClusterRole to a cluster, and neither reaches outside the cluster's own API server. If you get a Role wrong, the blast radius is a namespace. That's still bad, but it's bounded, and the boundary is structural, not a matter of discipline.
RAM has no equivalent hard wall. A RAM policy can grant access to ECS, OSS, RDS, PAI, and dozens of other product APIs in a single statement, because RAM's job is to govern an entire account's resources, not a single workload's namespace. There is no "cluster boundary" to fall back on: there's an account boundary, and everything inside it is potentially reachable by a permissive enough policy.
Here's a RAM policy that looks reasonable at a glance:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:Get*",
"oss:List*",
"ecs:Describe*"
],
"Resource": "*"
}
]
}
Nothing here says oss:Delete or ecs:Terminate. It looks read-only, and in a Kubernetes RBAC mindset, a read-only Role scoped to one namespace is low-stakes. But "Resource": "*" means this identity can list and read every OSS bucket and every ECS instance across the entire account, not one namespace's worth. A "read-only" mistake at account scope can still mean reading a bucket full of customer data that a different team, in a different project, forgot to lock down.
Where the RBAC analogy actively misleads
The vocabulary overlap isn't superficial by accident. Alibaba Cloud, like most cloud IAM systems, borrowed the role-and-policy language because it's a genuinely useful abstraction, and describing RAM as "roles with policies attached" is not wrong. The place the analogy breaks is scope inheritance. In Kubernetes, a Role bound in namespace payments cannot, under any binding, grant access to namespace billing. The API server enforces that structurally; there is no policy syntax that lets a Role escape its namespace. RAM has resource groups, which look like they play the same structural role, but a resource group is an organizational tag applied to resources, not an enforcement boundary the way a namespace is. A RAM policy scoped with "Resource": "*" ignores resource groups entirely and reaches across all of them, which means the thing that looks like Kubernetes' hard wall is, in RAM, an opt-in convention rather than a default enforcement mechanism.
This distinction matters most exactly when someone is moving fast, which is also when it's most likely to be skipped. Writing "Resource": "*" is faster than looking up the correct resource group ARN, and nothing in the API, the console, or a typical code review flags the difference, because both are syntactically valid RAM policies that Alibaba Cloud will apply without complaint.
The experiment
I built a small test: two identities, one styled as "the RBAC-minded version" (scoped tightly to a single application's resource group, mirroring how I'd write a Kubernetes Role) and one styled as "the RAM-native version" (a broader read-only grant across the account, which is closer to what I've seen shipped in real onboarding scripts because it's faster to write and passes fewer code reviews). I then asked a simple question of each: how many distinct OSS buckets and ECS instances can this identity enumerate across the account?
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["oss:Get*", "oss:List*", "ecs:Describe*"],
"Resource": "*",
"Condition": {
"StringEquals": {
"acs:ResourceGroupId": "rg-acfmxxxxxxxxxxx"
}
}
}
]
}
That's the RBAC-minded version: the same actions as the earlier example, but with a Condition block pinning the grant to a single resource group, the closest RAM equivalent to a namespace-scoped Role. The RAM-native version was the first policy shown above, unchanged, with no condition block at all, which is also the version I've most often found already deployed when auditing a real account.
The RBAC-minded identity, scoped to a resource group, could see exactly what I expected: the handful of resources tagged to that project.
$ aliyun oss ls --profile rbac-minded-identity
BucketName CreationDate Region
project-x-assets 2025-08-01T00:00Z cn-hangzhou
$ aliyun ecs DescribeInstances --profile rbac-minded-identity \
--query 'Instances.Instance[].InstanceId'
["i-bp1a2b3c4d5e6f7g8h"]
The RAM-native identity could enumerate every bucket and every instance across the account, including several that belonged to unrelated internal projects with no connection to the identity's stated purpose.
$ aliyun oss ls --profile ram-native-identity
BucketName CreationDate Region
project-x-assets 2025-08-01T00:00Z cn-hangzhou
billing-exports 2024-03-14T00:00Z cn-shanghai
ml-training-data 2025-01-09T00:00Z cn-beijing
legacy-backups-2022 2022-11-30T00:00Z cn-hangzhou
customer-uploads 2025-05-22T00:00Z cn-hangzhou
$ aliyun ecs DescribeInstances --profile ram-native-identity \
--query 'Instances.Instance[].InstanceId' | jq length
14
Fourteen instances and five buckets, against one instance and one bucket for the resource-group-scoped identity, using an action list that both policies shared verbatim. The only difference between the two policies was five lines of Condition block, and that difference was the entire security boundary.
The proof
Nothing about the second policy was a misconfiguration in the sense of a typo or a missing condition in an otherwise-complete policy; the condition simply wasn't there, and the policy engine had no reason to expect one. It was syntactically correct and semantically exactly what it said. The gap wasn't in the policy. It was in my assumption that RAM's default granularity matches RBAC's. It doesn't. RAM defaults to account-wide Resource: "*" unless you deliberately scope it down with resource group conditions or explicit ARNs, while Kubernetes RBAC defaults to namespace scope unless you deliberately widen it with a ClusterRole. The defaults point in opposite directions.
What this looks like from ActionTrail's side
The asymmetry doesn't just widen what an identity can read. It also widens what an audit trail has to distinguish between "was accessed" and "was accessible." Checking ActionTrail for the RAM-native identity after running the enumeration above surfaces every one of those Describe and List calls as individually unremarkable events:
{
"eventName": "ListBuckets",
"eventSource": "oss.aliyuncs.com",
"userIdentity": { "principalId": "ram-native-identity" },
"requestParameters": {},
"eventTime": "2025-11-03T09:14:22Z"
}
Each entry, read alone, looks like routine, permitted activity, because it is: the identity was authorized to make exactly this call. What ActionTrail's per-event log doesn't surface on its own is the aggregate fact that a single identity just enumerated five buckets and fourteen instances belonging to four unrelated projects in the space of a few seconds. That pattern is visible only if someone is looking for the account-wide reach of a single principal across resource groups, which is a different query than "did this API call succeed," and it's not the query most teams run by default. The RBAC-minded identity's equivalent audit trail is one line, one bucket, one instance, and there's no aggregate pattern to miss because there was nothing to aggregate.
You might disagree
You could argue this is just a documentation and training problem: Alibaba Cloud's own guidance recommends scoping RAM policies to resource groups, and anyone reading the docs carefully wouldn't make this mistake. That's true in principle. But RBAC's namespace default makes the safe behavior the path of least resistance; you have to actively reach for a ClusterRole to widen scope. RAM's account-wide default makes the unsafe behavior the path of least resistance; you have to actively reach for resource group conditions to narrow it. A system where the easy path is also the safe path fails less often than a system where they're the same amount of effort in opposite directions, regardless of how good the documentation is.
The stronger version of this counterargument, and the one worth taking seriously, is that Alibaba Cloud does provide tooling to close this gap: policy templates, the "principle of least privilege" advisor in the RAM console, and organization-level SCPs that can enforce resource group conditions centrally regardless of what an individual engineer writes. If a platform team enforces those centrally, the individual engineer's instinct to write "Resource": "*" never gets the chance to ship. That's a real mitigation, and it's the correct fix at an organizational level. But it's a fix that has to be deliberately built and maintained by a platform team; it isn't what RAM does by default, on day one, for an account that hasn't invested in that guardrail yet. The gap this article describes is exactly the condition every account starts in before someone builds that guardrail, and a lot of accounts never get there.
What I think now
I no longer treat "it looks like RBAC" as a reason to trust my RBAC instincts. Before writing any RAM policy now, I ask a question that doesn't have a Kubernetes equivalent: what resource groups, tags, or explicit ARNs does this policy need to be scoped to, given that the default is the entire account? That question doesn't exist in a namespace-scoped world, which is exactly why it's easy to forget to ask it.
I've also changed how I review policies written by other people, or generated by onboarding scripts and Terraform modules inherited from elsewhere. The review question isn't "does this action list look dangerous" (Get* and List* will always look benign next to Delete* and Terminate*). It's "what is the Resource field actually scoped to, and does that scope match what the identity's stated purpose requires." In every RAM policy audit I've run since, that one field has been the highest-signal thing to check, well above the action list, because the action list is usually written with real care and the resource scope is usually an afterthought, or a wildcard nobody meant permanently.
There's a second-order habit this produced too: I now write the resource-group condition first, before the action list, when drafting a new RAM policy, rather than writing the actions and coming back to scope it afterward. That ordering sounds trivial, but it changes what "done" feels like. Writing the actions first and the scope second makes the policy feel complete the moment the actions look right, and the scope becomes an optional tightening pass that's easy to skip under deadline pressure. Writing the scope first makes an unscoped policy feel visibly unfinished, which is a cheap way to make the safe habit slightly more automatic than the unsafe one, given that RAM itself won't do that for you.
The lesson learned
RAM and RBAC share vocabulary but not defaults. RBAC's namespace boundary makes narrow scope the path of least resistance; RAM's account-wide default makes broad scope the path of least resistance. Importing RBAC intuition into RAM policy design means importing a false sense of the blast radius you're actually working with, and the fix isn't more caution in general, it's a specific habit: read the Resource field first, assume account-wide reach until a condition proves otherwise, and treat every wildcard as a decision that needs to be justified rather than a default that needs no comment.