Home » Fix “End user credentials must match the user specified in the request.” in GCP

Fix “End user credentials must match the user specified in the request.” in GCP

In this article, we’ll discuss an error I ran into when trying to create SSH keys for a service account in Google Cloud Platform (GCP). Nevertheless, I assume it’s something that can happen in many situations where you’re trying to perform an operation as a service account. I hope to save you some time by documenting the solution.

The error that I ran into is the following:

googleapiclient.errors.HttpError: <HttpError 403 when requesting https://oslogin.googleapis.com/v1/users/<SERVICE_ACCOUNT>@<PROJECT_ID>.iam.gserviceaccount.com:importSshPublicKey?alt=json returned “End user credentials must match the user specified in the request.”. Details: “End user credentials must match the user specified in the request.”>

This is the function I adapted from the GCP example code. It initializes the oslogin API, creates SSH key files and attaches it to the service account.

def add_ssh_key(service_account_name, service_account_email, sa_credentials, credentials, **kwargs):
    # initialize the oslogin API
    oslogin = googleapiclient.discovery.build('oslogin', 'v1', credentials = credentials)

    # create a private & public ssh key file
    # see GCP example code for details on execute()
    private_key_file = f'/tmp/{service_account_name}-ssh-key-{str(uuid.uuid4())}'
    execute(['ssh-keygen', '-t', 'rsa', '-N', '', '-f', private_key_file])
    
    # load the public key file
    with open(private_key_file + '.pub', 'r') as original:
        public_key = original.read().strip()

    expiration = int((time.time() + 300) * 1000000)

    # load the public key_file into GCP
    body = {
        'key': public_key,
        'expirationTimeUsec': expiration,
    }
    oslogin.users().importSshPublicKey(
        parent = f'users/{service_account_email}',
        body = body).execute()

    return private_key_file
  
add_ssh_key(
	<SERVICE_ACCOUNT_NAME>, 
	<SERVICE_ACCOUNT_EMAIL>, 
  	os.environ['GOOGLE_APPLICATION_CREDENTIALS']
)

❗ Although I’ve added the full code for adding SSH keys, the mistake is in the first lines of the code. For the rest part of the article, I’ll focus on that.

Here’s what’s happening: during initializations, we’re passing it the credentials from the file which has its filename stored in the GOOGLE_APPLICATION_CREDENTIALS environment variable. Even if you didn’t pass anything to the credentials argument at all, you’d still run into the error, because the build method defaults to it.

More information here, here and here.

# Version 1: explicitly passing it the GOOGLE_APPLICATION_CREDENTIALS env variable
oslogin = googleapiclient.discovery.build('oslogin', 'v1', credentials = os.environ['GOOGLE_APPLICATION_CREDENTIALS'])
# Version 2: defaults to version 1
oslogin = googleapiclient.discovery.build('oslogin', 'v1')

👉 In other words, you’re probably mixing up (1) the credentials you use to authenticate from your app or local environment and (2) the credentials of the service account you’re trying to attach SSH keys to. When you want to add SSH keys to a service account, you need to be identified as the service account.

The solution is straightforward, it involves authenticating with the account you’re trying to add SSH keys for. You have multiple options:

  • Manually create and download the keys for the service account and set the file name in the GOOGLE_APPLICATION_CREDENTIALS environment variable when you’re adding the SSH keys. Tutorial over here.
  • You could impersonate as a service account. Documentation over here.
  • Create a service account key file using Python. This can be done using the code below.
def create_service_account_file(project_id, credentials, service_account_name, service_account_email, **kwargs):
    # initialize the iam API
    service = googleapiclient.discovery.build('iam', 'v1', credentials = credentials)
	
    # create and download keys
    key = service.projects().serviceAccounts().keys().create(
        name = f'projects/{project_id}/serviceAccounts/{service_account_email}', 
        body = {'privateKeyType': 'TYPE_GOOGLE_CREDENTIALS_FILE'}
    ).execute()
	
    # decode the key (or you'll run into the error described below)
    decoded_key = base64.b64decode(key['privateKeyData'])
	
    # store the key in a JSON file
    f = open(f'service-account-key-{service_account_name}.json', 'wb')
    f.write(decoded_key)
    f.close()

Make sure to include the part where you decode the key, or you’ll run into the following error:

ValueError: Service account info was not in the expected format, missing fields token_uri, client_email.

Now, during initialization of the oslogin API, make sure you identify as the service account.

service_account_credentials = google.oauth2.service_account.Credentials.from_service_account_file(f'service-account-key-{service_account_name}.json')
oslogin = googleapiclient.discovery.build('oslogin', 'v1', credentials = service_account_credentials)

Great success!

Say thanks, ask questions or give feedback

Technologies get updated, syntax changes and honestly… I make mistakes too. If something is incorrect, incomplete or doesn’t work, let me know in the comments below and help thousands of visitors.

1 thought on “Fix “End user credentials must match the user specified in the request.” in GCP”

Leave a Reply

Your email address will not be published. Required fields are marked *