Concentric IAMCredentials Permissions: The secret life of signBlob

2022-04-01

Did you know if you’re only given the IAM iam.serviceAccounts.signBlob permission you indirectly effectively also get iam.serviceAccounts.signJwt?

.AND..

if you have iam.serviceAccounts.signJwt you effectively also get iam.serviceAccounts.getAccessToken AND iam.serviceAccounts.getOpenIdToken?

…in other words, signBlob means you have signJwt and having signJwt means you can getAccessToken and getOpenIdToken:

concentric...

maybe you already know this but its important to realize that these are concentric and what that means when you to assign an over-provisioned GCP IAM Role like roles/iam.serviceAccountTokenCreator when you may not need all its capabilities.


so…how is signBlob the one ring to rule them all?

Well, a lot of it has to do with the fact that the ability to digitally sign using a service Account’s key is the basis for creating access_tokens and id_tokens using the oauth2 2LO flow

so, lets start off at the first level of concentric permissions:

How signBlob gives signJWT

This is easy: signedJWT is just a JWT that is signed by a service account…all you have to do is create a JWT and use the signBlob API and get a signedJWT…its the same JWT you’d get if you used the IAM API for a given key.

Done. Miller time

How signJWT gives generateIDToken or generateAccessToken

This is a bit more complicated and requires looking at the protocol used by a Service account to issue access_tokens and id_tokens:

if you look at the protocol described there, it all hinges on having service account sign a JWT with its private key and then exchange it with Google to get one of those tokens.

For access_token, the JWT payload will look like

{
  "iss": "svcaccount1@$PROJECT_ID.iam.gserviceaccount.com",
  "scope": "https://www.googleapis.com/auth/devstorage.read_only",
  "aud": "https://oauth2.googleapis.com/token",
  "exp": 1328554385,
  "iat": 1328550785
}

For id_tokens, the JWT payload will look like the following where target_audience is the final audience, issued value

{
  "iss": "svcaccount1@$PROJECT_ID.iam.gserviceaccount.com",
  "aud": "https://oauth2.googleapis.com/token",
  "exp": 1328554385,
  "iat": 1328550785,
  "target_audience": "https://foo.bar"
}

Once you have the raw JWT, you will sign it using the signBlob API….which in the end gives you a signedJWT.

Once you have the signedJWT, you can exchange that with the google oauth2 token endpoint for either an access_token or an id_token

In other words, the only real permission you need is the ability to signBlob…you can just use that ability to do the other capabilities.

If you want to look at this in another way from an API perspective since these methods map to permissions pretty much 1:1, its like this:

a stack of pancakes

Over-provisioned roles

So, if these permissions are concentric, what does that mean?

  • if all you need to use the iamCredentials API to impersonate service Account to just generate aid_tokens, do you need to assign it the tokenCreator role?

    no. Just assign a custom role with iam.serviceAccounts.getOpenIdToken

  • if all you need to use the iamCredentials API to impersonate service Account to just generate an access_token, do you need to assign it the tokenCreator role?

    no. Just assign a custom role with iam.serviceAccounts.getAccessToken

The other thing you’d want to understand is what other roles are using signBlob?

For that, you can use the a public dataset i wrote a bit ago to enumerate the roles that include that: Google Cloud IAM Roles-Permissions Public Dataset

For signBlob:

$ bq query --nouse_legacy_sql  '
SELECT
  r1
FROM
  iam-log.iam.permissions AS d1, UNNEST(roles) r1
WHERE
  d1._PARTITIONTIME = TIMESTAMP("2022-04-01")
  AND d1.name = "iam.serviceAccounts.signBlob"
  AND d1.region = "us-central1"
'
+-----------------------------------------+
|                   r1                    |
+-----------------------------------------+
| roles/aiplatform.customCodeServiceAgent |
| roles/cloudfunctions.serviceAgent       |
| roles/appengineflex.serviceAgent        |
| roles/dataflow.serviceAgent             |
| roles/iam.serviceAccountTokenCreator    |
| roles/pubsub.serviceAgent               |
| roles/ml.serviceAgent                   |
| roles/run.serviceAgent                  |
| roles/serverless.serviceAgent           |
+-----------------------------------------+

Now lets see what other permissions are included because knowing that signBlob will allow other permissions

bq query --nouse_legacy_sql  '
SELECT
  p1, d1.name
FROM
  iam-log.iam.roles AS d1,
  UNNEST(included_permissions) p1
WHERE
  d1._PARTITIONTIME = TIMESTAMP("2022-04-01")
  AND d1.name in UNNEST(["roles/aiplatform.customCodeServiceAgent", "roles/cloudfunctions.serviceAgent","roles/appengineflex.serviceAgent","roles/dataflow.serviceAgent","roles/iam.serviceAccountTokenCreator","roles/pubsub.serviceAgent","roles/ml.serviceAgent","roles/run.serviceAgent","roles/serverless.serviceAgent"])
  AND d1.region = "us-central1"
  AND p1 in UNNEST(["iam.serviceAccounts.signBlob", "iam.serviceAccounts.signJwt","iam.serviceAccounts.getAccessToken","iam.serviceAccounts.getOpenIdToken"])
'

+------------------------------------+-----------------------------------------+
|                 p1                 |                  name                   |
+------------------------------------+-----------------------------------------+
| iam.serviceAccounts.getAccessToken | roles/ml.serviceAgent                   |
| iam.serviceAccounts.getOpenIdToken | roles/ml.serviceAgent                   |
| iam.serviceAccounts.signBlob       | roles/ml.serviceAgent                   |
| iam.serviceAccounts.signJwt        | roles/ml.serviceAgent                   |
| iam.serviceAccounts.getAccessToken | roles/run.serviceAgent                  |
| iam.serviceAccounts.getOpenIdToken | roles/run.serviceAgent                  |
| iam.serviceAccounts.signBlob       | roles/run.serviceAgent                  |
| iam.serviceAccounts.getAccessToken | roles/pubsub.serviceAgent               |
| iam.serviceAccounts.getOpenIdToken | roles/pubsub.serviceAgent               |
| iam.serviceAccounts.signBlob       | roles/pubsub.serviceAgent               |
| iam.serviceAccounts.signJwt        | roles/pubsub.serviceAgent               |
| iam.serviceAccounts.getAccessToken | roles/dataflow.serviceAgent             |
| iam.serviceAccounts.signBlob       | roles/dataflow.serviceAgent             |
| iam.serviceAccounts.signJwt        | roles/dataflow.serviceAgent             |
| iam.serviceAccounts.getAccessToken | roles/serverless.serviceAgent           |
| iam.serviceAccounts.getOpenIdToken | roles/serverless.serviceAgent           |
| iam.serviceAccounts.signBlob       | roles/serverless.serviceAgent           |
| iam.serviceAccounts.getAccessToken | roles/appengineflex.serviceAgent        |
| iam.serviceAccounts.signBlob       | roles/appengineflex.serviceAgent        |
| iam.serviceAccounts.signJwt        | roles/appengineflex.serviceAgent        |
| iam.serviceAccounts.getAccessToken | roles/cloudfunctions.serviceAgent       |
| iam.serviceAccounts.getOpenIdToken | roles/cloudfunctions.serviceAgent       |
| iam.serviceAccounts.signBlob       | roles/cloudfunctions.serviceAgent       |
| iam.serviceAccounts.getAccessToken | roles/iam.serviceAccountTokenCreator    |
| iam.serviceAccounts.getOpenIdToken | roles/iam.serviceAccountTokenCreator    |
| iam.serviceAccounts.signBlob       | roles/iam.serviceAccountTokenCreator    |
| iam.serviceAccounts.signJwt        | roles/iam.serviceAccountTokenCreator    |
| iam.serviceAccounts.getAccessToken | roles/aiplatform.customCodeServiceAgent |
| iam.serviceAccounts.getOpenIdToken | roles/aiplatform.customCodeServiceAgent |
| iam.serviceAccounts.signBlob       | roles/aiplatform.customCodeServiceAgent |
| iam.serviceAccounts.signJwt        | roles/aiplatform.customCodeServiceAgent |
+------------------------------------+-----------------------------------------+

From the above, note that

  • roles/run.serviceAgent does not include iam.serviceAccounts.signJwt but it can already do that anyway
  • roles/dataflow.serviceAgent does not include iam.serviceAccounts.getOpenIdToken but it can already do that anyway
  • roles/serverless.serviceAgent does not include iam.serviceAccounts.signJwt but it can already do that anyway
  • roles/appengineflex.serviceAgent does not include iam.serviceAccounts.getOpenIdToken but it can already do that anyway
  • roles/cloudfunctions.serviceAgent does not include iam.serviceAccounts.signJwt but it can already do that anyway

Setup

So…don’t take my word for all this, see for yourself with this demo:

The following code will crate a serviceAccount, allow you to impersonate that account and then…using just signBlob iamcredentials api, we will:

  1. generate a JWT suitable for access_token exchange
  2. sign it with signBlob
  3. exchange that signedJWT for an access_token
  4. generate a JWT suitable for id_token exchange
  5. sign it with signBlob
  6. exchange that signedJWT for an id_token
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
export GCLOUD_USER=`gcloud config get-value core/account`

gcloud iam service-accounts create svcaccount1

gcloud iam service-accounts add-iam-policy-binding  \
   svcaccount1@$PROJECT_ID.iam.gserviceaccount.com  \
   --member=user:$GCLOUD_USER  \
   --role=roles/iam.serviceAccountTokenCreator

Now run the app:

go run main.go --svcAccountEmail=svcaccount1@$PROJECT_ID.iam.gserviceaccount.com

Perpetual tokens with self-impersonation

Ok, so now we know signBLob give you a lot of permissions..more than you expected. One additional thing you may want to know about unanticipated permission with the TokenCreator role is issues with long-term tokens with “service Account self impersonation”

Self impersonation is a trick thats sometimes necessary to allow you to create SignedURL with Cloud Run, Cloud Functions and GCE VMs. What this allows a GCE VM that can’t otherwise signBLob to do is to create those signedURLs…your’e basically giving it a synthetic, remote key.

Now…whats the problem with self-impersonation and signBlob? Well, if a service account will need to use its own access_token to call the signBlob API….but if a service account can do that, it can also effectively get a new access_token too.

What if a valid access_token is leaked….now the attacker can keep using that access_token to generate a new token just before the previous one expires…you now have a long-term token leaked from a measly access_token

the clock from that show i didn't understand...

You might be thinking…can’t i just place a VPC-SC for the IAM api around the GCE VM where i’m using this self-impersonation? well, the iamcredential api isn’t supported.

Moral of this is…be careful and aware when using self-impersonation…


Appendix

Also see,


This site supports webmentions. Send me a mention via this form.



comments powered by Disqus