Skip to content

Latest commit

 

History

History
346 lines (269 loc) · 17.2 KB

README.md

File metadata and controls

346 lines (269 loc) · 17.2 KB

Using serviceAccountActor IAM role for account impersonation on Google Cloud Platform

12/1/21 STOP...i'm just leaving this repo up for archive. I wrote this article long ago before GCP had formal client library support. if you want to use GCP libraries for impersonation, see https://github.com/salrashid123/gcp_impersonated_credentials

DEPRECATED!!!

The serviceAccountActor IAM role on Google Cloud has some very useful and powerful capabilities. It is an IAM role that allows you to grant another user or serviceAccount the ability to impersonate a service Account. In this way, you can have service account A impersonate B and acquire the access_tokens or id_tokens for B.

This article covers how you can acquire id and access tokens for service account B by service account A. You can also have a end-user assigned the serviceAccountActor role too. For clarity, I've separated out the id and access token steps below.

Note:

  • access_tokens are used to gain access to a Google API as B.
  • id_tokens are used to identify the the service Account B to some other system. The ID token contains digitally signed information that can be validated to assert who the token was issued to.
    • Google issued id_token: these id_tokens are signed directly by Google's Certificates and simply assert the callers identity.

One final mechanism described here is creating an arbitrary JWT using Service Account's private key. These JWTs are signed by one of the keys associated with a given service account and can convey additional verifiable information within the claim such as the intended target (audience) and custom scopes.

For more information see

You can find the script referenced here on my github page

Assign serviceAccountActor role

The first step is to assign the serviceAccountActor role on B to A. That is, you are allowing A the permission to act as B. This configuration is done on the IAM Permissions page by selecting B as shown below.

images/svc.png

In the example screenshot,

Note: serviceAccountB_ID does not even have any valid certificate keys. This means there are no valid physical, distributed certificate key files.

As mentioned, you do NOT have to use a service account to impersonate another one; you can easily assign the serviceAccountActor role to an end user (e.g. [email protected]).

Initialize an IAM client for service Account A

Since we are doing operations as service Account A, we need to create a client for access to IAM using Application Default Credentials:

import httplib2
from apiclient.discovery import build
from oauth2client.client import GoogleCredentials
scope = 'https://www.googleapis.com/auth/iam https://www.googleapis.com/auth/cloud-platform'
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "ServiceAccountA_keyFile.json"
credentials = GoogleCredentials.get_application_default()
if credentials.create_scoped_required():
   credentials = credentials.create_scoped(scope)
http = credentials.authorize(httplib2.Http())

service = build(serviceName='iam', version= 'v1',http=http)
resource = service.projects()

Create access_token

After we assigned the role, download the certificate for A (the service you will use to impersonate B). You may also impersonate B as a user and to do that, you will need to initialize a client as an end user. For more information on, see Google Cloud Authentication Samples.

Create an unsigned JWT

Now that we have an initialized, authenticated client for A, we need to generate a JWT with some specific claims. Since we are creating an access_token request, the JWT needs to be in the form:

{
    "iss": "serviceAccountB_ID",
    "scope": "scope1 scope2",
    "aud": "https://accounts.google.com/o/oauth2/token",
    "exp":  expiration_time,
    "iat":  issue_time
}    

Set the scopes to limit the capabilities of a given token. A list of scopes can be found here.

Acquire the JWT using Service Account A credentials

Now that we have an unsigned JWT claim set, we need to sign it using A's credentials but instruct .signBlob() to sign it for B:

client_id= 'serviceAccountB_ID'
slist = resource.serviceAccounts().signJwt(name='projects/your-project/serviceAccounts/' + client_id, 
                                                 body={'payload': claim })
resp = slist.execute()

signed_jwt = resp['signedJwt']

Transmit the singed JWT to Google to get an access_token

url = 'https://accounts.google.com/o/oauth2/token'
data = {'grant_type' : 'assertion',
        'assertion_type' : 'http://oauth.net/grant_type/jwt/1.0/bearer',
        'assertion' : signed_jwt }
headers = {"Content-type": "application/x-www-form-urlencoded"}
data = urllib.urlencode(data)
req = urllib2.Request(url, data, headers)
resp = urllib2.urlopen(req).read()
parsed = json.loads(resp)
access_token = parsed.get('access_token')

Now you have an access token. With this, you can access any GoogleAPI that is scoped correctly and to which B has access.

One way to initialize a Google API client given a raw access_token is to use oauth2client.client.AccessTokenCredentials. Other languages have similar bindings.

You can also verify the access_token by interrogating the tokeninfo endpoint as shown here:

curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=ya29.ElnUAws0MfV3om_tqQ0ZS3g7-BZ2ZS_uaJUNdPk8TI7hHkPnFb8Kesg67WQwJswTfD1nYTk-tD0MDUf6X3-pKklpicBDZszq16Q7smBZoDemnYcWaYvJ8gjqnw

{
 "azp": "104943293997971332223",
 "aud": "104943293997971332223",
 "scope": "https://www.googleapis.com/auth/userinfo.email",
 "exp": "1484452560",
 "expires_in": "3543",
 "email": "[email protected]",
 "email_verified": "true",
 "access_type": "offline"
}

images/access_token.png

Google issued id_token

Though id_tokens and access_tokens have similar flows, they are used for very different things: as the name implies, access_tokens are used to grant access to google resources (eg. GCS, PubSub, etc) while id_tokens simply assert the bearers identity.

These id_tokens are digitally signed by google so given an id_token, a system can verify its authenticity by verifying its signature against Google's public certificate. The id_tokens are simply convey who the caller is (not what it can do).

If you need two system to communicate securely, you can pass (via SSL), an id_token. The receiving system can locally validate the token using a cached copy of Google's certificate. However, Google-issued ID tokens simply identify the caller and cannot assert if the receipient of the token is the intended target. For that, you can mint a Service Account id_token as described below.

See:

images/id_token.png

Create an unsigned JWT

The first step is to create an unsigned JWT but have the scope to the service Account B's ID:

{
    "iss": "serviceAccountB_ID",
    "scope": "serviceAccountB_ID",
    "aud": "https://www.googleapis.com/oauth2/v4/token",
    "exp":  expiration_time,
    "iat":  issue_time
}

Note the audience is different than for an access_token.

Acquire the JWT using Service Account A credentials

Now that we have an JWT claim set, we need to get a jwt using A's credentials but instruct .signJwt()

client_id= 'serviceAccountB_ID'
slist = resource.serviceAccounts().signJwt(name='projects/your-project/serviceAccounts/' + client_id, 
                                                  body={'payload': id_token_claim })
resp = slist.execute()     
signed_jwt = resp['signedJwt']

Transmit the signed JWT to Google to get an id_token

url = 'https://www.googleapis.com/oauth2/v4/token'
data = {'grant_type' : 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion' : signed_jwt }
headers = {"Content-type": "application/x-www-form-urlencoded"}
     
data = urllib.urlencode(data)
req = urllib2.Request(url, data, headers)
resp = urllib2.urlopen(req).read()
parsed = json.loads(resp)
id_token = parsed.get('id_token')

Now that we have the id_token, we can transmit it to another system.

id_token validation

The recieving system that gets the id_token must validate its authenticity.

You can validate it using some standard libraries too:

from oauth2client.client import verify_id_token
from oauth2client.crypt import AppIdentityError
try:
  jwt = verify_id_token(id_token, client_id)     
  self.log('\n ID_TOKEN Validation: \n ' + json.dumps(jwt,sort_keys = False, indent = 4)  +' \n', logging.INFO)
except AppIdentityError, e:
  self.log('Payload: ' + str(e.read), logging.ERROR) 

The public certificate used by Google to sign can be found at:

Service Account signed JWT

Users can also generate a JWT that is signed by a given Service Account. The steps here are very simple: create a claim and use .signJWT() to sign. The signedJWTs can be used to verify caller identity as well as see if the recipient is the intended target. Finally, you can customize the scopes fields to convey capability for the token.

You can specify the intended target for the JWT using the aud field. In this case, the audience is 'System C'. The scopes field is arbitrary and user-defined.

audience = 'SystemC'
id_scope='scope1 scope2'
now = int(time.time())
exptime = now + 3600
id_token_claim =('{"iss":"%s","scope":"%s", "aud":"%s","exp":%s,"iat":%s}') %(client_id,id_scope,audience,exptime,now)    

slist = resource.serviceAccounts().signJwt(name='projects/your-project/serviceAccounts/' + client_id, 
                                                  body={'payload': id_token_claim })
resp = slist.execute()
signed_jwt = resp['signedJwt']

images/id_token.png

The JWT issued may look like the following:

{
  "alg": "RS256",
  "typ": "JWT"
}.
{
  "iss": "[email protected]",
  "scope": "scope1 scope2",
  "aud": "SystemC",
  "exp": 1484508490,
  "iat": 1484504890
}

A GCP service account has multiple keys that are rotated. Unless the current keyID is specified in the JWT header, you need to iterate the keys in the keystore URL (shown below) to verify the correct one.

If the key used to sign is: 'cc1080d1a4c61e8cb821331a5a2652dee2c901a1', you may add on the 'kid' header value to the JWT.

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "cc1080d1a4c61e8cb821331a5a2652dee2c901a1"
}

Once you have the JWT, you must verify the authenticity of the JWT by verifying against the public certificate. The public certs for any Google Service account is visible at URLs similar to: For serviceAccount B:

The following node sample verfies a self-signed JWT:

var jwt = require('jsonwebtoken');
var jose = require('node-jose');
var request = require("request")

var jwk_url = 'https://www.googleapis.com/service_accounts/v1/jwk/[email protected]';
skeyid = 'cc1080d1a4c61e8cb821331a5a2652dee2c901a1';
// A GCP service account has multiple keys that are rotated.  Unless the current keyID is specified in the JWT header, you need to iterate 
// the keys in the keystore to verify the correct one.
//skeyid = '5e0918577a1a33e87bfc8621c5914d4f035b91a5';
var sjwt ='eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZXJ2aWNlLWFjY291bnQtYkBtaW5lcmFsLW1pbnV0aWEtODIwLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJzY29wZTEgc2NvcGUyIiwgImF1ZCI6IlN5c3RlbUMiLCJleHAiOjE0ODQ1MTIxNTIsImlhdCI6MTQ4NDUwODU1Mn0.Ishj6r8-cdOA8kI8aslaKAF23Q8cct3HARUe-TB2YU2cK59_5SoIt7s_MMCG9Ea-PG0LfdGweMBep39d8c-aQS6KE_H2ls1dff990W0lodU_QUrHOZJksSzLMHfjFDRgs3LgF83O4F0VZ50rTrYN0mmwjw1d7EHv7LqHA4TAOQs3bhKte9K3csiYETR3HLqbAvdiO3jog0vWUGU57Mu4DkjWWSvPXnHQv-YWWzeHm5fAIsLiRW1LB9a7gdLZPwbSLlCRs6WrbWtGMuGax6ouBcqE24YzL0YbdH7E6mkM5qE8LeHnUL2e8ZNFBZdDc6y6Q4wq2-S9xEIN7hbk0uYrEQ';
var decoded = jwt.decode(sjwt,{complete: true});
var payload = decoded.payload;
// need to verify the token expiration date (todo)
var edate = new Date(payload.exp*1000);
// also verify the audience (intended target) (todo)
// https://www.npmjs.com/package/node-jose#verifying-a-jws
// for now go get the public cert to atleast verify the signature.
var request = require("request")
request({
    url: jwk_url,
    json: true
}, function (error, response, body) {
    if (!error && response.statusCode === 200) {
      jose.JWK.asKeyStore(body).
        then(function(result) {
          keystore = result;
          key = keystore.get(skeyid);
          jose.JWS.createVerify(keystore).verify(sjwt)
               .then(function(result) { 
                    console.log('Signature Verification Suceeded')
                    console.log(result);
                  }, function(error) {
                    console.log('Signature Verification Failed');
                  }
               );
          });
    }
})

access_token/id_token script

You can find the script referenced here on my github page. To use it, you need to download a certificate for service Account A, assign the serviceAccountActor role to it for B, then install the libraries and invoke it with service account B's ID value

virtualenv env 
source env/bin/activate
pip install requests google-api-python-client httplib2 oauth2client
$python gcs_auth.py --client_id=service-account-b@your-project.iam.gserviceaccount.com
[2017-01-15 11:29:12.072293] access_token: ya29.ElnUA-HwLE2hVH-y0iz5CgGTywcJNP63eTAI216i7e6cC66vXUQd-X87MvVheZ4ZQlwE31Uv_fOJTZ62hiK8pWFt_rN5xc54ZHCzGB4_8pvrZAJGrolLDYEvPw

[2017-01-15 11:29:12.450104] id_token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjAxYTEwMmYyOGNmOGYzNzk3MTVlZDU5ZWU5ODAzY2VkNTViMDMxOTYifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJpYXQiOjE0ODQ1MDg1NTIsImV4cCI6MTQ4NDUxMjE1MiwiYXVkIjoic2VydmljZS1hY2NvdW50LWJAbWluZXJhbC1taW51dGlhLTgyMC5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6IjEwNDk0MzI5Mzk5Nzk3MTMzMjIyMyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhenAiOiJzZXJ2aWNlLWFjY291bnQtYkBtaW5lcmFsLW1pbnV0aWEtODIwLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwiZW1haWwiOiJzZXJ2aWNlLWFjY291bnQtYkBtaW5lcmFsLW1pbnV0aWEtODIwLmlhbS5nc2VydmljZWFjY291bnQuY29tIn0.P0juBBi-GGkGnpKR5FaKKXBWxrFsxk25TRqP5fbXklPpMgn5QmAidwt6NG2qK-sE5ebVIj-1EEA7gTub83PK-CCUN4KGDjwhY7Egoodyppdz2TAW0Q5ObKsVUHtwgFNUf_9xSaxorCS202yLcontWjYpRihFFYGrfdvLFOdyeJZ3Iz4NAQdfU_Ouk1bjFt7M-gfvJgIaAQrxNfyhnwsdEDLRd-Dn53V3P73IRAneLuAl2wzQLPYKb2uRCe0u9kjQEjTRQu19tWjLBx59no9nxjXwKEQpFhZm4KqSSIR6xOJYvf3_m0oOwrC8mDGeOLoV8sis1ravtBCp3e9oESl1-g

[2017-01-15 11:29:12.595477]  ID_TOKEN Validation: 
 {
    "aud": "[email protected]", 
    "iss": "https://accounts.google.com", 
    "email_verified": true, 
    "exp": 1484512152, 
    "azp": "[email protected]", 
    "iat": 1484508552, 
    "email": "[email protected]", 
    "sub": "104943293997971332223"
} 

[2017-01-15 11:29:12.693403] Self-signed id_token:: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZXJ2aWNlLWFjY291bnQtYkBtaW5lcmFsLW1pbnV0aWEtODIwLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJzY29wZTEgc2NvcGUyIiwgImF1ZCI6IlN5c3RlbUMiLCJleHAiOjE0ODQ1MTIxNTIsImlhdCI6MTQ4NDUwODU1Mn0.Ishj6r8-cdOA8kI8aslaKAF23Q8cct3HARUe-TB2YU2cK59_5SoIt7s_MMCG9Ea-PG0LfdGweMBep39d8c-aQS6KE_H2ls1dff990W0lodU_QUrHOZJksSzLMHfjFDRgs3LgF83O4F0VZ50rTrYN0mmwjw1d7EHv7LqHA4TAOQs3bhKte9K3csiYETR3HLqbAvdiO3jog0vWUGU57Mu4DkjWWSvPXnHQv-YWWzeHm5fAIsLiRW1LB9a7gdLZPwbSLlCRs6WrbWtGMuGax6ouBcqE24YzL0YbdH7E6mkM5qE8LeHnUL2e8ZNFBZdDc6y6Q4wq2-S9xEIN7hbk0uYrEQ