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

Preventing perpetual tokens with self-impersonation

One potential issue with self-impersonation is that if a service account is allowed to impersonate itself, an access_token it has can be used within an hour to issue a new access_token. Then that one can be used to get a new one and so on within an hour (i.,e before the previous token expires).

However GCP prevents cycling impersonated tokens by detecting the source and preventing the chained use:

For example, see in the following how the chained use of the token is prohibited (with a useless error but thats ok)

export PROJECT_ID=`gcloud config get-value core/project`
export GCLOUD_USER=`gcloud config get-value core/account`

gcloud config set core/log_http_redact_token false

gcloud iam service-accounts create svcacctcycle

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

gcloud iam service-accounts add-iam-policy-binding  \
    --role roles/iam.serviceAccountTokenCreator  \
    --member "serviceAccount:svcacctcycle@$PROJECT_ID.iam.gserviceaccount.com"  \
    svcacctcycle@$PROJECT_ID.iam.gserviceaccount.com

gcloud auth print-access-token \
   --impersonate-service-account svcacctcycle@$PROJECT_ID.iam.gserviceaccount.com > /tmp/sa_1.txt

gcloud auth revoke

gcloud auth print-access-token \
  --impersonate-service-account svcacctcycle@$PROJECT_ID.iam.gserviceaccount.com \
  --access-token-file=/tmp/sa_1.txt  > /tmp/sa_2.txt


gcloud auth print-access-token \
   --impersonate-service-account svcacctcycle@$PROJECT_ID.iam.gserviceaccount.com \
   --access-token-file=/tmp/sa_2.txt

{
  "error": {
    "code": 403,
    "message": "The caller does not have permission",
    "status": "PERMISSION_DENIED"
  }
}

Appendix

Also see,


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