# How to replace JSON key with X.509 WiF This short guide provides step by step instructions how to replace a JSON key with a Workload Identity Federation (WiF) X.509 certificate. ## Prerequisites - `openssl` 3.x - Node.js with `npm` - `gcloud` authenticated as an administrator of the target project The examples use the project ID `my-project` and the project number `123456789012`. Replace them with your own values. ## 1. Create the certificates The script creates a root CA, an issuing CA and a client certificate. The client certificate subject CN (`test-client`) becomes the WiF subject. ```bash # Root CA certificate echo "Generating Root CA certificate..." openssl req \ -x509 \ -newkey rsa:4096 \ -keyout ca_key.pem \ -out ca_cert.pem \ -days 3650 \ -noenc \ -subj "/CN=Test CA $(date +"%Y-%m-%d")" \ -addext "basicConstraints=critical,CA:TRUE,pathlen:1" \ -addext "keyUsage=critical,keyCertSign,cRLSign" # Issuing CA certificate echo "Generating Issuing CA certificate..." openssl req \ -newkey rsa:4096 \ -keyout issuing_ca_key.pem \ -noenc \ -subj "/CN=Issuing Test CA $(date +"%Y-%m-%d")" \ -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \ -addext "keyUsage=critical,keyCertSign,cRLSign" | openssl x509 \ -req \ -CA ca_cert.pem \ -CAkey ca_key.pem \ -copy_extensions copyall \ -days 3650 \ -out issuing_ca_cert.pem # Client certificate echo "Generating Client certificate..." openssl req \ -newkey rsa:4096 \ -keyout client_key.pem \ -noenc \ -subj "/CN=test-client" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature,keyEncipherment" \ -addext "extendedKeyUsage=clientAuth" | openssl x509 \ -req \ -CA issuing_ca_cert.pem \ -CAkey issuing_ca_key.pem \ -copy_extensions copyall \ -days 365 \ -out client_cert.pem ``` ## 2. Create the trust store The trust store is a YAML file that holds the root CA as the trust anchor and the issuing CA as an intermediate CA. The generator uses the `yaml` npm package. `package.json`: ```json { "dependencies": { "yaml": ">=2.9.1" } } ``` `make_trust_store.mjs`: ```javascript #!/usr/bin/env node import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { parseArgs } from "node:util"; import YAML from "yaml"; const { values } = parseArgs({ options: { "ca-cert": { type: "string", default: "ca_cert.pem" }, "issuing-ca-cert": { type: "string", default: "issuing_ca_cert.pem" }, output: { type: "string", default: "trust_store.yaml" }, }, }); // IMPORTANT: Strip the trailing newline from the PEM files, // otherwise gcloud will reject the trust store with a cryptic error message. const trustStore = { trustAnchors: [{ pemCertificate: readFileSync(values["ca-cert"], "utf8").trimEnd() }], }; // The issuing CA is optional; it is included only when its file exists. if (existsSync(values["issuing-ca-cert"])) { trustStore.intermediateCas = [ { pemCertificate: readFileSync(values["issuing-ca-cert"], "utf8").trimEnd() }, ]; } writeFileSync(values.output, YAML.stringify({ trustStore }, { indentSeq: false })); ``` Install the dependency and generate `trust_store.yaml`: ```bash npm install node make_trust_store.mjs ``` The PEM files must contain only the certificate block. A trailing newline after `-----END CERTIFICATE-----` makes gcloud fail with `Certificate is not in PEM format`, which is why the script trims it. ## 3. Create the pool, provider and credentials The script creates the workload identity pool, the X.509 provider and a service account. It allows the client certificate subject to impersonate the service account, grants the service account read access to the project, creates a storage bucket with read and write access to its objects for the service account, enables Data Access audit logs for Cloud Storage, and writes `cred.json`. It then logs in with `cred.json` in a separate gcloud configuration, checks the access, uploads and downloads a file, and lists the audit log of the service account operations. Data Access audit logs are enabled for the whole project, not per bucket, by editing the project IAM policy. `add_audit_config.mjs` adds the `auditConfigs` section to the policy file fetched by gcloud. The policy must be fetched as YAML, because a different default gcloud output format makes `set-iam-policy` reject the file. `add_audit_config.mjs`: ```javascript #!/usr/bin/env node import { readFileSync, writeFileSync } from "node:fs"; import YAML from "yaml"; const policy = YAML.parse(readFileSync("iam_policy.yaml", "utf8")); // Replace the Cloud Storage entry, if any, and keep the other audit configs. policy.auditConfigs = (policy.auditConfigs ?? []).filter( (c) => c.service !== "storage.googleapis.com", ); policy.auditConfigs.push({ service: "storage.googleapis.com", auditLogConfigs: [{ logType: "DATA_READ" }, { logType: "DATA_WRITE" }], }); writeFileSync("iam_policy.yaml", YAML.stringify(policy, { indentSeq: false })); ``` `make_wif.sh`: ```bash # Create a workload identity pool and a X.509 provider gcloud iam workload-identity-pools create my-pool \ --project=my-project \ --location=global \ --display-name="My Pool" # Create X.509 provider gcloud iam workload-identity-pools providers create-x509 my-x509 \ --project=my-project \ --location=global \ --workload-identity-pool=my-pool \ --trust-store-config-path=trust_store.yaml \ --attribute-mapping="google.subject=assertion.subject.dn.cn" # Create the service account gcloud iam service-accounts create sa-test \ --project=my-project \ --display-name="SA Test" # Allow impersonation of the service account gcloud iam service-accounts add-iam-policy-binding \ sa-test@my-project.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/my-pool/subject/test-client" # Grant the service account read access to the project gcloud projects add-iam-policy-binding my-project \ --member="serviceAccount:sa-test@my-project.iam.gserviceaccount.com" \ --role=roles/browser # Create credentials configuration files gcloud iam workload-identity-pools create-cred-config \ projects/123456789012/locations/global/workloadIdentityPools/my-pool/providers/my-x509 \ --service-account=sa-test@my-project.iam.gserviceaccount.com \ --credential-cert-path=client_cert.pem \ --credential-cert-private-key-path=client_key.pem \ --output-file=cred.json # Create a storage bucket gcloud storage buckets create gs://my-project-wif-test \ --project=my-project \ --location=europe-central2 \ --uniform-bucket-level-access # Allow the service account to read and write objects in the bucket gcloud storage buckets add-iam-policy-binding gs://my-project-wif-test \ --member="serviceAccount:sa-test@my-project.iam.gserviceaccount.com" \ --role=roles/storage.objectUser # Enable Data Access audit logs for Cloud Storage (project-wide, not per bucket) gcloud projects get-iam-policy my-project --format=yaml > iam_policy.yaml node add_audit_config.mjs gcloud projects set-iam-policy my-project iam_policy.yaml # Create a new gcloud configuration for the workload identity federation test gcloud config configurations create wif-test # Authenticate gcloud with the credentials configuration file gcloud auth login --cred-file=cred.json # Set the project to my-project gcloud config set project my-project # Test the authentication by listing the project details gcloud auth list gcloud projects describe my-project gcloud projects get-iam-policy my-project \ --flatten="bindings[].members" \ --format="table(bindings.role, bindings.members)" # Test the bucket access by uploading and downloading a file gcloud storage ls gs://my-project-wif-test gcloud storage cp README.md gs://my-project-wif-test/README.md gcloud storage ls gs://my-project-wif-test gcloud storage cp gs://my-project-wif-test/README.md README.downloaded.md cmp README.md README.downloaded.md # Then, switch back to your original account gcloud config configurations activate default # List the audit log of the operations made by the service account gcloud logging read \ 'protoPayload.authenticationInfo.principalEmail="sa-test@my-project.iam.gserviceaccount.com"' \ --project=my-project \ --freshness=1h \ --order=asc \ --format="table(timestamp, protoPayload.serviceName, protoPayload.methodName)" # and again switch back to the service account gcloud config configurations activate wif-test ``` The audit log lists only Cloud Storage operations of the service account. Resource Manager calls such as `projects describe` are not covered by the Data Access configuration. ## Result `cred.json` is a credentials configuration file that references the client certificate and private key. It contains no secret itself, but `client_key.pem` does and must be protected like a JSON key.