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
openssl3.x- Node.js with
npm gcloudauthenticated 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.
# 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:
{
"dependencies": {
"yaml": ">=2.9.1"
}
}
make_trust_store.mjs:
#!/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:
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, and writes cred.json. It then logs in with cred.json and checks the access.
# 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
# Authenticate gcloud with the credentials configuration file
gcloud auth login --cred-file=cred.json
# 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)"
# Then, switch back to your original account
gcloud config set account admin@example.com
# and again switch back to the service account using the credentials configuration file
gcloud config set account sa-test@my-project.iam.gserviceaccount.com
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.