The 3 permissions in Google Cloud you can escalate to do anything

Alexey Inkin
8 min readMar 15, 2024

--

A permission escalation is when you can’t do something but are allowed to modify your own permissions so you can.

Service Account Almighty.

resourcemanager.projects.setIamPolicy: Become the project owner

The permission resourcemanager.projects.setIamPolicy lets you grant roles on a project. You may think it can only share the access you have yourself, i.e. only grant the roles in which you have every permission on the project level.

This is not the case. You can grant any role to anyone, including yourself. This example shows it.

To see this, create a service account and grant it a custom role with only the two permissions to get and set the policy:

$ gcloud iam service-accounts create "test-1"
Created service account [test-1].
$ gcloud iam service-accounts keys create key.json \
--iam-account=test-1@$PROJECT.iam.gserviceaccount.com

created key [0e61d692b3f12d8a5b2051f2a75a8db25accc765] of type [json] as
[key.json] for [test-1@project-id.iam.gserviceaccount.com]
$ gcloud iam roles create Test1 \
--permissions=resourcemanager.projects.setIamPolicy,resourcemanager.projects.getIamPolicy \
--stage=GA

Created role [Test1].
etag: BwYTic5Oc5s=
includedPermissions:
- resourcemanager.projects.getIamPolicy
- resourcemanager.projects.setIamPolicy
name: projects/project-id/roles/Test1
stage: GA
title: Test1
$ gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:test-1@$PROJECT.iam.gserviceaccount.com" \
--role="projects/$PROJECT/roles/Test1"

Updated IAM policy for project [project-id].
bindings:
- members:
- serviceAccount:test-1@project-id.iam.gserviceaccount.com
role: projects/project-id/roles/Test1
etag: BwYTidGhDQY=
version: 1
$ gcloud auth revoke --all

Sign in with the new service account and try to create a Cloud Storage bucket. It will fail because the permissions you have do not allow it:

$ gcloud auth activate-service-account --key-file=key.json
Activated service account credentials for:
[test-1@project-id.iam.gserviceaccount.com]
$ gcloud storage buckets create "gs://$PROJECT-test1"
Creating gs://project-id-test1/...
ERROR: (gcloud.storage.buckets.create) HTTPError 403:
test-1@project-id-test1.iam.gserviceaccount.com does not have
storage.buckets.create access to the Google Cloud project.
Permission 'storage.buckets.create' denied on resource (or it may not exist).

But these permissions allow you to become the project owner:

$ gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:test-1@$PROJECT.iam.gserviceaccount.com" \
--role='roles/owner'

Updated IAM policy for project [project-id].
bindings:
- members:
- serviceAccount:test-1@project-id.iam.gserviceaccount.com
role: projects/project-id/roles/Test1
- members:
- serviceAccount:test-1@project-id.iam.gserviceaccount.com
role: roles/owner
etag: BwYTikPia9U=
version: 1

And then you can certainly create a bucket, which you could not do earlier:

$ gcloud storage buckets create "gs://$PROJECT-test1"
Creating gs://project-id-test1/...

In this example, we needed two permissions to get and set policy. This is because gcloud needs to read the policy before applying the changes. For the direct REST call to set the policy, the ‘set’ permission alone will do.

resourcemanager.organizations.setIamPolicy: Become the organization owner

You guessed it. resourcemanager.organizations.setIamPolicy is the ultimate beast permission to take over your entire organization just the same way.

iam.roles.update: Enjoy all permissions if you have any custom role

The permission iam.roles.update lets you update custom roles. You may think it can only grant those permissions to a role that you yourself have on the project level for a project role and on the organization level for an organization role.

This is not the case. You can grant any permission to a role, including the permissions you don’t have to a role that you have.

To see this, create a service account and grant it a custom role with only the two permissions to get and update roles:

$ gcloud iam service-accounts create "test-2"
Created service account [test-2].
$ gcloud iam service-accounts keys create key.json \
--iam-account=test-2@$PROJECT.iam.gserviceaccount.com

created key [37fcfa449049e4ab44e40e1a9beab00c3d26276a] of type [json] as
[key2.json] for [test-2@project-id.iam.gserviceaccount.com]
$ gcloud iam roles create Test2 \
--permissions=iam.roles.get,iam.roles.update \
--stage=GA

Created role [Test2].
etag: BwYTm0OCSu0=
includedPermissions:
- iam.roles.get
- iam.roles.update
name: projects/project-id/roles/Test2
stage: GA
title: Test2
$ gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:test-2@$PROJECT.iam.gserviceaccount.com" \
--role="projects/$PROJECT/roles/Test2"

Updated IAM policy for project [project-id].
bindings:
- members:
- serviceAccount:test-2@project-id.iam.gserviceaccount.com
role: projects/project-id/roles/Test2
etag: BwYTm0_Mbsk=
version: 1
$ gcloud auth revoke --all

Sign in with the new service account and try to create a Cloud Storage bucket. It will fail because the permissions you have do not allow it:

$ gcloud auth activate-service-account --key-file=key.json
Activated service account credentials for:
[test-2@project-id.iam.gserviceaccount.com]
$ gcloud storage buckets create "gs://$PROJECT-test2"
Creating gs://project-id-test2/...
ERROR: (gcloud.storage.buckets.create) HTTPError 403:
test-2@project-id.iam.gserviceaccount.com does not have
storage.buckets.create access to the Google Cloud project.
Permission 'storage.buckets.create' denied on resource (or it may not exist).

But these permissions allow you to add the missing permission to the custom role you have:

$ gcloud iam roles update Test2 --add-permissions=storage.buckets.create
etag: BwYTm35Gekg=
includedPermissions:
- iam.roles.get
- iam.roles.update
- storage.buckets.create
name: projects/project-id/roles/Test2
stage: GA
title: Test2

And then you can certainly create a bucket, which you could not do earlier:

$ gcloud storage buckets create "gs://$PROJECT-test2"
Creating gs://project-id-test2/...

In this example, we needed two permissions to get and update the role. This is because gcloud needs to read the role before applying the changes. For the direct REST call to update the role, the ‘update’ permission alone will do.

What makes it worse

Counter-intuitive

When you design a system from the ground up, you naturally would not allow this.

Poorly documented

I only found a few modest mentions like this one in the REST documentation:

AI chat bots deny this problem

Nowadays, people learn everything through AI chat bots. It’s OK for many problems, but it’s a terrible idea to learn security matters this way. As of writing, many chat bots tell you that those permissions are safe, including Google’s own Gemini!

The Microsoft’s bot was the only one who warned me:

Other clouds work the same way

It’s not something specific to Google. Amazon Web Services works the same way.

To see this, create a JSON file with this policy:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreatePolicyVersion"
],
"Resource": "*"
}
]
}

Then create a policy and assign it to a new user:

$ aws iam create-policy --policy-name Test2Policy \
--policy-document file://policy.json

{
"Policy": {
"PolicyName": "Test2Policy",
"PolicyId": "policy-id",
"Arn": "arn:aws:iam::aws-account-id:policy/Test2Policy",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 0,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"CreateDate": "2024-03-15T04:42:54+00:00",
"UpdateDate": "2024-03-15T04:42:54+00:00"
}
}
$ aws iam create-user --user-name test-2
{
"User": {
"Path": "/",
"UserName": "test-2",
"UserId": "user-id",
"Arn": "arn:aws:iam::aws-account-id:user/test-2",
"CreateDate": "2024-03-15T04:39:54+00:00"
}
}
$ aws iam create-access-key --user-name test-2
{
"AccessKey": {
"UserName": "test-2",
"AccessKeyId": "access-key-id",
"Status": "Active",
"SecretAccessKey": "secret-access-key",
"CreateDate": "2024-03-15T04:40:30+00:00"
}
}
$ aws iam attach-user-policy \
--policy-arn arn:aws:iam::aws-account-id:policy/Test2Policy \
--user-name test-2

Sign in as that user and try to create a bucket. It will fail because you don’t have this action in your policy:

$ aws configure
AWS Access Key ID [********************]: access-key-id
AWS Secret Access Key [********************]: secret-access-key
Default region name [us-east-1]:
Default output format [json]:
$ aws s3 mb s3://my-bucket-name
make_bucket failed: s3://my-bucket-name An error occurred
(AccessDenied) when calling the CreateBucket operation: Access Denied

Add the missing action to the policy file:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreatePolicyVersion",
"s3:CreateBucket"
],
"Resource": "*"
}
]
}

And update your own policy like this:

$ aws iam create-policy-version \
--policy-arn arn:aws:iam::aws-account-id:policy/Test2Policy \
--policy-document file://policy.json --set-as-default

{
"PolicyVersion": {
"VersionId": "v2",
"IsDefaultVersion": true,
"CreateDate": "2024-03-15T05:00:38+00:00"
}
}

Now you can create a bucket, which you could not do earlier:

$ aws s3 mb s3://my-bucket-name
make_bucket: my-bucket-name

Practical implications

There is a compelling CI/CD pattern of three role levels:

Say you want to create a stage on each PR to run tests on it. At the top, you have a central service account that can create projects, project-creator. On each PR, it creates a Google Cloud project. This account is so powerful that it should be used as little as possible.

Therefore, when a project for a PR is created, you should make a service account limited to the project to set everything up, it’s called deploy. It configures all services and creates resources. It also creates the least powerful accounts for runners like pods, cloud functions, etc.

Each runner has the bare minimal permissions to access what it requires down to specific message queues, subscriptions, and tables.

If not for the problem of permission escalation, deploy account could be created with a sum of the permissions for the runners and the ability to share them. Then, if a key for a deploy account is compromised, there would be a limit to what can be done with it.

However, since deploy must create roles for the runners and assign them, it has everything needed to become almighty if its key is leaked. Therefore, there’s no point making deploy anything less than an Owner of the project it manages. So much for the principle of least privilege at the deployment stage.

How the cloud vendors can do better

Describe the permissions

With AWS, in the policy, we explicitly list the allowed actions, and each action has a dedicated page like this. That’s good.

Google Cloud has an indirection here, a permission. While a permission in most cases maps to a REST call and back, it’s another entity to look for, and permissions are often hard to find:

Nothing is found for “roles.update” in the REST reference for IAM service in Google Cloud.

This leads to people not checking the security details once they find a method that works for them.

A dedicated page on escalation

Write out all potential ways to escalate permissions. I assume my article is not exhausting. A lot of other objects have setIamPolicy permissions.

Link to that page with red banners on the pages of affected methods, roles, and permissions.

The permission reference not only does not have dedicated pages for permissions, but also has no permission-specific text at all. This lack of learning data leads to chat bots constantly confusing roles and permissions.

A dedicated page is what will be caught by AI chat bots, and they will finally start doing better on security questions.

Make unescalatable permissions to share permissions

We could run CI/CD workflows with much lower privileges if there were calls to share access that can’t escalate. I’m thinking of the following permissions:

  • resourcemanager.projects.selfConstrainedSetIamPolicy
  • resourcemanager.organizations.selfConstrainedSetIamPolicy
  • iam.roles.selfConstrainedUpdate

Thanks.

Never miss a story, follow:

--

--

Alexey Inkin

Google Developer Expert in Flutter. PHP, SQL, TS, Java, C++, professionally since 2003. Open for consulting & dev with my team. Telegram channel: @ainkin_com