196 lines
6.6 KiB
Markdown
196 lines
6.6 KiB
Markdown
# 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, 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 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
|
|
|
|
# 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.
|