Uploaded image for project: 'Bitbucket Cloud'
  1. Bitbucket Cloud
  2. BCLOUD-21138

Support Workload Identity Federation for GCP Service Accounts

    • Our product teams collect and evaluate feedback from a number of different sources. To learn more about how we use customer feedback in the planning process, check out our new feature policy.

      All current documentation showcases using GCP Service Account JSON Keys for deploying with Bitbucket Pipelines Deployment. But these are notoriously insecure and hard to maintain and keep rotated.

      GCP has several different solutions detailed here. I think the right solution is for Bitbucket to support Workload Identity Federation. Bitbucket supplies the ID Token, and we authenticate our Service Account(s) with it. Then there are no keys to manage/secure/rotate.

            [BCLOUD-21138] Support Workload Identity Federation for GCP Service Accounts

            Patrick Nelson added a comment - - edited

            Here's a simplified version of what I'm doing to pull private images from GCP and then run my code in them (see below for an explanation and more use cases, e.g. building). This is using OIDC + Workload Identity Federation and no private keys. Just setup Workload Identity in GCP (use my instructions from a few years ago) and then edit the environment variables at the top.

            options:
              docker: true
            
            pipelines:
              branches:
                main:
                  - step:
                      name: Pull private image and run code
                      image: google/cloud-sdk:alpine
                      caches:
                        - docker
                      script:
                        # Set up environment variables (adjust as needed)
                        - export CREDENTIAL_CONFIG_FILE=credential-config.json # Configuration file from GCP for your specific Workload Identity provider and service account
                        - export OIDC_TOKEN_FILE=/tmp/oidc-token.txt # The path you gave (also in the config above) which tells gcloud to look for the OIDC token from Bitbucket
                        - export CONTAINER_IMAGE=us-docker.pkg.dev/your-project-id/your-repo/your-image:latest # Image to pull and run
                        - export CONTAINER_HOSTNAME=us-docker.pkg.dev # Hostname for the container registry
                        - export WORK_DIR=/var/www/html # Location to mount current code directory in container
                        - export SCRIPT_FILE=script.sh # Script to run in container (relative to WORK_DIR)
            
                        # Login with OIDC using gcloud and the token provided by Bitbucket
                        - echo "$BITBUCKET_STEP_OIDC_TOKEN" > $OIDC_TOKEN_FILE
                        - gcloud auth login --cred-file=$CREDENTIAL_CONFIG_FILE
                        - gcloud auth configure-docker -q $CONTAINER_HOSTNAME
            
                        # Pull the private image (the docker cache above will speed this up)
                        - docker pull $CONTAINER_IMAGE
            
                        # Run the image
                        - docker run -v "$BITBUCKET_CLONE_DIR:$WORK_DIR" $CONTAINER_IMAGE $WORK_DIR/$SCRIPT_FILE 

            What this is doing

            Since we can't pull our private image natively from the top level and then run the code normally in the step, we switch to running the code "Docker-in-Docker". So, we start instead with a publicly available image which has the tools we need to login first (e.g. google/cloud-sdk:alpine, which has gcloud, but google/cloud-sdk:latest has even more). Once you're authenticated, you can then pull your private images and run your code in them, or access other restricted resources as you normally would with your service account.

            Keep in mind that it's your credential configuration file (which I've called credential-config.json above and below) that glues everything together:

            • Specifies which service account will be used
            • Points to your Workload Identity Provider in GCP (under Workload Identity Federation -> Workload Identity Pools). Remember also that the Workload Identity Provider is GCP's own reference back to Bitbucket.
            • Tells gcloud where to find the OIDC token that Bitbucket generated for this step, which it then uses to login (this token should be kept private).

            Also note that while your credential configuration file isn't sensitive, the OIDC token that Bitbucket generates is sensitive. This is because it's exchanged by gcloud for a short-lived access token which effectively logs you in temporarily as (or "impersonates") the configured service account. This access token has a lifetime of 1 hour by default, but you can constrain it further by generating your credential config from the command line.

            What this enables

            • You can run your code in private images. In this case you have to move it to a script file if it's multiple lines (see basic example above) instead of having the code directly in the step.
            • You can build images which reference private images. Just change docker run to docker build.
            • You can perform other deploy actions which would normally require "gcloud auth login" as well. You'd have to perform the above anyway, since Bitbucket could most likely only ever potentially natively support pulling private images (similar to how they integrate with AWS).

             

            Hopefully this helps others looking for a workaround until this gets supported (if ever).

            Patrick Nelson added a comment - - edited Here's a simplified version of what I'm doing to pull private images from GCP and then run my code in them (see below for an explanation and more use cases , e.g. building). This is using OIDC + Workload Identity Federation and no private keys. Just setup Workload Identity in GCP (use my instructions from a few years ago) and then edit the environment variables at the top. options: docker: true pipelines: branches: main: - step: name: Pull private image and run code image: google/cloud-sdk:alpine caches: - docker script: # Set up environment variables (adjust as needed) - export CREDENTIAL_CONFIG_FILE=credential-config.json # Configuration file from GCP for your specific Workload Identity provider and service account - export OIDC_TOKEN_FILE=/tmp/oidc-token.txt # The path you gave (also in the config above) which tells gcloud to look for the OIDC token from Bitbucket - export CONTAINER_IMAGE=us-docker.pkg.dev/your-project-id/your-repo/your-image:latest # Image to pull and run - export CONTAINER_HOSTNAME=us-docker.pkg.dev # Hostname for the container registry - export WORK_DIR=/ var /www/html # Location to mount current code directory in container - export SCRIPT_FILE=script.sh # Script to run in container (relative to WORK_DIR) # Login with OIDC using gcloud and the token provided by Bitbucket - echo "$BITBUCKET_STEP_OIDC_TOKEN" > $OIDC_TOKEN_FILE - gcloud auth login --cred-file=$CREDENTIAL_CONFIG_FILE - gcloud auth configure-docker -q $CONTAINER_HOSTNAME # Pull the private image (the docker cache above will speed this up) - docker pull $CONTAINER_IMAGE # Run the image - docker run -v "$BITBUCKET_CLONE_DIR:$WORK_DIR" $CONTAINER_IMAGE $WORK_DIR/$SCRIPT_FILE What this is doing Since we can't pull our private image natively from the top level and then run the code normally in the step, we switch to running the code "Docker-in-Docker". So, we start instead with a publicly available image which has the tools we need to login first (e.g. google/cloud-sdk:alpine, which has gcloud, but google/cloud-sdk:latest has even more). Once you're authenticated, you can then pull your private images and run your code in them, or access other restricted resources as you normally would with your service account. Keep in mind that it's your credential configuration file (which I've called credential-config.json above and below) that glues everything together: Specifies which service account will be used Points to your Workload Identity Provider in GCP (under Workload Identity Federation -> Workload Identity Pools). Remember also that the Workload Identity Provider is GCP's own reference back to Bitbucket. Tells gcloud where to find the OIDC token that Bitbucket generated for this step, which it then uses to login ( this token should be kept private ). Also note that while your credential configuration file isn't sensitive, the OIDC token that Bitbucket generates  is sensitive. This is because it's exchanged by gcloud for a short-lived access token which effectively logs you in temporarily as (or "impersonates") the configured service account. This access token has a lifetime of 1 hour by default, but you can constrain it further by generating your credential config from the command line . What this enables You can run your code in private images. In this case you have to move it to a script file if it's multiple lines (see basic example above) instead of having the code directly in the step. You can build images which reference private images. Just change docker run to docker build . You can perform other deploy actions which would normally require "gcloud auth login" as well. You'd have to perform the above anyway, since Bitbucket could most likely only ever potentially natively support pulling private images (similar to how they integrate with AWS).   Hopefully this helps others looking for a workaround until this gets supported (if ever).

            benja315 added a comment -

            Following the current guidance and blog posts between GCP and Bitbucket is difficult and does not show how to implement Workload Identity Federation using best practices. The current guidance should be expanded to show usage of the principal identities in GCP to use a service account. This leads then to this issue where this could be better provided as a standardized integration within Bitbucket itself. This is a great opportunity to lead engineers into a secure by design solution. I also recommend leveraging Pipeline OIDC tokens and GCP Secrets Manager to support third party API integrations that still do not support OIDC. Most other platform guidance is to set repository variables and these are difficult to find and rotate.

            benja315 added a comment - Following the current guidance and blog posts between GCP and Bitbucket is difficult and does not show how to implement Workload Identity Federation using best practices. The current guidance should be expanded to show usage of the principal identities in GCP to use a service account. This leads then to this issue where this could be better provided as a standardized integration within Bitbucket itself. This is a great opportunity to lead engineers into a secure by design solution. I also recommend leveraging Pipeline OIDC tokens and GCP Secrets Manager to support third party API integrations that still do not support OIDC. Most other platform guidance is to set repository variables and these are difficult to find and rotate.

            Patrick Nelson added a comment - - edited

            p.s. Since I didn't explicitly mention it below (nor in the main description per se), it'd be awesome if this meant Pipelines could support GCP WIF/OIDC at the step level (e.g. when performing the image pulling). This would be similar to how Pipelines already supports AWS, by providing a service key under the "image" where you can define your OIDC configuration. But instead of "aws" it would be maybe GCP-specific like "gcp" e.g.

             

            pipelines:
              default:
                - step:
                    image:
                       name: gcr.io/some-project-name/some-image:1.2.3
                       gcp:
                          credential-config: ./path/to/service-account-credential-config.json
                          token-path: /tmp/oidc-token.txt
                    oidc: true
                    script:
                      - echo "hello world"

            Where service-account-credential-config.json would be the credential configuration stored in the current repository (which is fine, it doesn't contain any secrets) and the token-path would be the path that the JSON points to (or you could just read the JSON file yourself to get it, actually 😅). Anyway... then that should authorize your own container that has the ability to login and pull the image.

             

            Patrick Nelson added a comment - - edited p.s. Since I didn't explicitly mention it below (nor in the main description per se), it'd be awesome  if this meant Pipelines could support GCP WIF/OIDC at the  step  level (e.g. when performing the image pulling). This would be similar to how Pipelines already supports AWS, by providing a service key under the "image" where you can define your OIDC configuration. But instead of "aws" it would be maybe GCP-specific like " gcp " e.g.   pipelines: default : - step: image: name: gcr.io/some-project-name/some-image:1.2.3 gcp: credential-config: ./path/to/service-account-credential-config.json token-path: /tmp/oidc-token.txt oidc: true script: - echo "hello world" Where service-account-credential-config.json would be the credential configuration stored in the current repository (which is fine, it doesn't contain any secrets) and the token-path would be the path that the JSON points to (or you could just read the JSON file yourself to get it, actually 😅). Anyway... then that should authorize your own container that has the ability to login and pull the image.  

            Kindly ignore above as issue has been resolved with help of Norbert Csupka from Bitbucket support. issue was missing below in my pipeline.

             
            oidc: true

            Jagendra Atal Prakash added a comment - Kindly ignore above as issue has been resolved with help of Norbert Csupka from Bitbucket support. issue was missing below in my pipeline.   oidc: true

            Jagendra Atal Prakash added a comment - - edited

            Thanks for the details. I have tried all these steps but could not make it work. Is there anything missing from these steps? I have used GOOGLE_APPLICATION_CREDENTIALS with the generated/downloaded file. When I echo echo "$BITBUCKET_STEP_OIDC_TOKEN"  then I get it empty and as suggested not used these rather directly used GOOGLE_APPLICATION_CREDENTIALS with sample generated file. Kindly suggest what am I missing. Also I could not find these documents.

            │ Error: Failed to get existing workspaces: querying Cloud Storage failed: Get "https://storage.googleapis.com/storage/v1/b/tf-state-integration-develop/o?alt=json&delimiter=%2F&pageToken=&prefix=terraform%2Fstate%2F&prettyPrint=false&projection=full&versions=false": oauth2/google: unable to generate access token: Post "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/terraform@integration-develop.iam.gserviceaccount.com:generateAccessToken": oauth2/google: failed to open credential file "/tmp/oidc-token.txt"
            
            Error: Failed to get existing workspaces: querying Cloud Storage failed: Get "https://storage.googleapis.com/storage/v1/b/tf-state-integration-develop/o?alt=json&delimiter=%2F&pageToken=&prefix=terraform%2Fstate%2F&prettyPrint=false&projection=full&versions=false": oauth2/google: unable to generate access token: Post "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/terraform@integration-develop.iam.gserviceaccount.com:generateAccessToken": oauth2/google: status code 400: {"error":"invalid_request","error_description":"subject_token must be nonempty."}
            
            

            Jagendra Atal Prakash added a comment - - edited Thanks for the details. I have tried all these steps but could not make it work. Is there anything missing from these steps? I have used GOOGLE_APPLICATION_CREDENTIALS with the generated/downloaded file. When I echo echo "$BITBUCKET_STEP_OIDC_TOKEN"  then I get it empty and as suggested not used these rather directly used GOOGLE_APPLICATION_CREDENTIALS with sample generated file. Kindly suggest what am I missing. Also I could not find these documents. │ Error: Failed to get existing workspaces: querying Cloud Storage failed: Get "https: //storage.googleapis.com/storage/v1/b/tf-state-integration-develop/o?alt=json&delimiter=%2F&pageToken=&prefix=terraform%2Fstate%2F&prettyPrint= false &projection=full&versions= false " : oauth2/google: unable to generate access token: Post "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/terraform@integration-develop.iam.gserviceaccount.com:generateAccessToken" : oauth2/google: failed to open credential file "/tmp/oidc-token.txt" │ Error: Failed to get existing workspaces: querying Cloud Storage failed: Get "https: //storage.googleapis.com/storage/v1/b/tf-state-integration-develop/o?alt=json&delimiter=%2F&pageToken=&prefix=terraform%2Fstate%2F&prettyPrint= false &projection=full&versions= false " : oauth2/google: unable to generate access token: Post "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/terraform@integration-develop.iam.gserviceaccount.com:generateAccessToken" : oauth2/google: status code 400: { "error" : "invalid_request" , "error_description" : "subject_token must be nonempty." }

            Patrick Nelson added a comment - - edited

            Alright folks! 🎉 I've done a bunch of research and some tinkering and here's what I found works for me. I'm going to just jot down what I did so that anyone else stumbling upon this can hit the ground running as fast as possible. Maybe someday I'll write up a longer Medium article with some graphs and stuff explaining what this is doing, why it works and etc. But for now, this should at least give some guidance on how to move forward.

            A few things to note:

            1. These are just a brain dump of MY notes, but you may find them useful. So, I've bolded the parts below that you may want to customize further. Also of course: This is probably incomplete and may not exactly match what you're trying to do.
            2. I decided to restrict my service account down to only a specific Bitbucket deployment environment (e.g. in my case, Production)
            3. I have a pedantic nomenclature, so you can choose your own names below.

            With the above out of the way, here are my steps:

            • In Bitbucket:
              • Go into repository settings and navigate to OpenID Connect
              • Select the “Production” deployment environment
              • Taken note of the "Identity Provider URL" and the audience values as well as the selected deployment environment UUID
            • In GCP
              • Create workload identity pool called “bitbucket-pipelines-pool1
              • Add an OIDC provider, call it “bitbucket-pipelines-provider1”. This provider will need to vary from one Bitbucket workspace to another (if you have multiple workspaces). However, it works from the workspace level and down.
              • Copy/paste the “Identity Provider URL” from Bitbucket settings from earlier into the “Issuer (URL)” field
              • Select “Allowed audiences” and paste the audience value from Bitbucket as well. Now that this is unique to your particular workspace.
              • In attribute mappings (i.e. mapping from Bitbucket's value in the OIDC token to something you can reference on Google's side):
                • For “google subject” paste “assertion.sub”
                • Add “attribute.deployment_environment_uuid” (a custom name for use in GCP) and for the OIDC value, paste “assertion.deploymentEnvironmentUuid” (feel free to customize this or to add/remove your own attributes that you want to restrict)
              • Save the provider
              • Navigate to "bitbucket-pipelines-pool1" and click “Grant access”
              • Select the service account to be used
              • Select “Only identities matching the filter”
              • Choose the custom attribute created earlier, called “deployment_environment_uuid” and paste the UUID of the deployment environment selected earlier (or whatever other field[s] you added above)
                • Note: Alternatively you could constrain this more loosely, like to the repository, or more strictly, like down to the individual step level.
                • Tip: You can get your step UUID via $BITBUCKET_STEP_UUID in the pipeline. Just map the step in attribute mappings (e.g. attribute.step_uuid to Bitbucket's assertion.stepUuid)
              • Click Save. 
              • In the next window, you'll be presented with a modal "Configure your application". This will give you a user-friendly UI version of the gcloud iam workload-identity-pools create-cred-config command.
              • Just select your newly created provider (bitbucket-pipelines-provider1)
              • Enter /tmp/oidc-token.txt into the "OIDC ID token path" field.
              • Ensure "Format type" stays "text" since $BITBUCKET_STEP_OIDC_TOKEN is just a plain text token.
              • Download the config file, name it credential-config.json and put into the root of your repo (of course actual location will vary for you!)
            • In bitbucket-pipelines.yml
              • Add a step like:   echo "$BITBUCKET_STEP_OIDC_TOKEN" > /tmp/oidc-token.txt
                • Important: Be sure to keep this OIDC token safe; treat it like a password, because it can be exchanged by gcloud for a short-lived access token with the ability to login as your service account. This access token has a lifetime of 1 hour by default, but you can constrain it further by generating your credential config from the command line.
              • Remove/replace the step "gcloud auth activate-service-account..." with instead:   gcloud auth login --cred-file=credential-config.json

            Everything else is roughly the same.

             

            Useful reading material/resources:

            Patrick Nelson added a comment - - edited Alright folks! 🎉 I've done a bunch of research and some tinkering and here's what I found works for me. I'm going to just jot down what I did so that anyone else stumbling upon this can hit the ground running as fast as possible. Maybe someday I'll write up a longer Medium article with some graphs and stuff explaining what this is doing, why it works and etc. But for now, this should at least give some guidance on how to move forward. A few things to note: These are just a brain dump of MY notes, but you may find them useful. So, I've bolded  the parts below that you may want to customize further. Also of course: This is probably incomplete and may not exactly match what you're trying to do. I decided to restrict my service account down to only a specific Bitbucket deployment environment (e.g. in my case, Production) I have a pedantic nomenclature, so you can choose your own names below. With the above out of the way, here are my steps: In Bitbucket: Go into repository settings and navigate to OpenID Connect Select the “ Production ” deployment environment Taken note of the "Identity Provider URL" and the audience values as well as the selected deployment environment UUID In GCP Create workload identity pool called “ bitbucket-pipelines-pool1 ” Add an OIDC provider, call it “ bitbucket-pipelines-provider1 ”. This provider will need to vary from one Bitbucket workspace to another (if you have multiple workspaces). However, it works from the workspace level and down. Copy/paste the “Identity Provider URL” from Bitbucket settings from earlier into the “Issuer (URL)” field Select “Allowed audiences” and paste the audience value from Bitbucket as well. Now that this is unique to your particular workspace. In attribute mappings ( i.e. mapping from Bitbucket's value in the OIDC token to something you can reference on Google's side ): For “google subject” paste “assertion.sub” Add “attribute.deployment_environment_uuid” (a custom name for use in GCP) and for the OIDC value, paste “assertion.deploymentEnvironmentUuid” ( feel free to customize this or to add/remove your own attributes that you want to restrict ) Save the provider Navigate to " bitbucket-pipelines-pool1 " and click “Grant access” Select the service account to be used Select “Only identities matching the filter” Choose the custom attribute created earlier, called “ deployment_environment_uuid ” and paste the UUID of the deployment environment selected earlier (or whatever other field [s] you added above) Note : Alternatively you could constrain this more loosely, like to the repository, or more strictly, like down to the individual step level. Tip : You can get your step UUID via $BITBUCKET_STEP_UUID in the pipeline. Just map the step in attribute mappings (e.g. attribute.step_uuid to Bitbucket's assertion.stepUuid ) Click Save.  In the next window, you'll be presented with a modal "Configure your application". This will give you a user-friendly UI version of the gcloud iam workload-identity-pools create-cred-config command. Just select your newly created provider (bitbucket-pipelines-provider1) Enter /tmp/oidc-token.txt into the "OIDC ID token path" field. Ensure "Format type" stays "text" since $BITBUCKET_STEP_OIDC_TOKEN is just a plain text token. Download the config file, name it credential-config.json and put into the root of your repo ( of course actual location will vary for you! ) In bitbucket-pipelines.yml Add a step like:    echo "$BITBUCKET_STEP_OIDC_TOKEN" > /tmp/oidc-token.txt Important: Be sure to keep this OIDC token safe; treat it like a password, because it can be exchanged by gcloud for a short-lived access token with the ability to login as your service account. This access token has a lifetime of 1 hour by default, but you can constrain it further by generating your credential config from the command line . Remove/replace the step "gcloud auth activate-service-account..." with instead:   gcloud auth login --cred-file= credential-config.json NOTE: If you use GOOGLE_APPLICATION_CREDENTIALS , just point that environment variable to this file instead. No need for "gcloud auth..." if you're already using that variable. See here for the difference between the two: https://stackoverflow.com/questions/52376634/difference-between-gcloud-auth-activate-service-account-key-file-and-google-ap Everything else is  roughly  the same.   Useful reading material/resources: General https://bitbucket.org/blog/bitbucket-pipelines-and-openid-connect-no-more-secret-management https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/ https://www.youtube.com/watch?v=4vajaXzHN08 - Very dense, but explains high level how the process works gcloud commands https://cloud.google.com/sdk/gcloud/reference/iam/workload-identity-pools/create-cred-config - sets up credential configuration (with OIDC we retain configuration instead of actual credentials) https://cloud.google.com/sdk/gcloud/reference/auth/login#--cred-file - logs in immediately with the configuration (instead of activating a service account) https://github.com/salrashid123/gcpcompat-oidc - happened to have lots of examples of various commands that could potentially be useful (in case you prefer CLI instead of the GUI)

            Patrick Nelson added a comment - - edited

            Yeah I'd love to learn more about this too Jared! 

            Are you referring to the OIDC documentation for Bitbucket Pipelines, correct? I presume this means the pipelines themselves can login (or run as) some kind of Bitbucket user maintained within Bitbucket and then you can somehow link/authenticate that account with your GCP service account so it can then impersonate that account once GCP validates it somehow?

            Edit: Is this the documentation you ended up using? https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/

            Edit 2: See below. 😅

            Patrick Nelson added a comment - - edited Yeah I'd love to learn more about this too Jared!  Are you referring to the OIDC documentation for Bitbucket Pipelines, correct? I presume this means the pipelines themselves can login (or run as) some kind of Bitbucket user maintained within Bitbucket and then you can somehow link/authenticate that account with your GCP service account so it can then impersonate that account once GCP validates it somehow? Edit: Is this the documentation you ended up using? https://support.atlassian.com/bitbucket-cloud/docs/integrate-pipelines-with-resource-servers-using-oidc/ Edit 2: See below. 😅

            kdgcwood added a comment -

            Can you link to the OIDC documentation?

            kdgcwood added a comment - Can you link to the OIDC documentation?

            This can be closed. I found the OIDC documentation, even though the GCP examples were non-existent (probably because it doesn't work with gcloud sdk).

            Jared Markell added a comment - This can be closed. I found the OIDC documentation, even though the GCP examples were non-existent (probably because it doesn't work with gcloud sdk).

              Unassigned Unassigned
              5ce39b3ae978 Jared Markell
              Votes:
              18 Vote for this issue
              Watchers:
              11 Start watching this issue

                Created:
                Updated: