diff --git a/README.md b/README.md new file mode 100644 index 0000000..7da1e62 --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# 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, and writes `cred.json`. It then logs in with `cred.json` and checks the access. + +```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 + +# 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. diff --git a/make_certs.sh b/make_certs.sh index b9d45fa..b401056 100755 --- a/make_certs.sh +++ b/make_certs.sh @@ -12,7 +12,7 @@ openssl req \ -addext "keyUsage=critical,keyCertSign,cRLSign" # Issuing CA certificate -echo +echo "Generating Issuing CA certificate..." openssl req \ -newkey rsa:4096 \ -keyout issuing_ca_key.pem \