diff --git a/README.md b/README.md index 1141730..4cb6a55 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This short guide provides step by step instructions how to replace a JSON key wi ## Configuration -The scripts read the names of the resources from `env.sh`. Replace the example values with your own before running them. +The scripts read the names of the resources from `env.sh`. Replace the example values with your own before running them. Run all scripts from the repository directory. `env.sh`: @@ -32,69 +32,23 @@ BUCKET=my-project-wif-test BUCKET_LOCATION=europe-central2 ``` +## About the shell scripts + +The `.sh` files are meant as references for copying and pasting the commands one by one, so that each step can be inspected and adapted. Running them directly with `bash` also works. + +> **Note:** The scripts are not idempotent. Running a whole script with `bash` a second time may fail, because the resources created by the first run already exist, for example the workload identity pool, the provider, the service account, the bucket and the `wif-test` gcloud configuration. + ## 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. - -`make_certs.sh`: +`make_certs.sh` creates a root CA, an issuing CA and a client certificate. The client certificate subject CN (`test-client`) becomes the WiF subject. ```bash -source ./env.sh - -# 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 +bash make_certs.sh ``` ## 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`: +`make_trust_store.mjs` creates `trust_store.yaml`, a YAML file that holds the root CA as the trust anchor and the issuing CA as an intermediate CA. It uses the `yaml` npm package, declared in `package.json`: ```json { @@ -104,38 +58,6 @@ The trust store is a YAML file that holds the root CA as the trust anchor and th } ``` -`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 @@ -147,134 +69,14 @@ The PEM files must contain only the certificate block. A trailing newline after ## 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, makes the public folder of the bucket publicly readable, 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`: +`make_wif.sh` 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, makes the public folder of the bucket publicly readable, and lists the audit log of the service account operations. ```bash -source ./env.sh - -# Create a workload identity pool and a X.509 provider -gcloud iam workload-identity-pools create "${POOL_ID}" \ - --project="${PROJECT_ID}" \ - --location=global \ - --display-name="${POOL_DISPLAY_NAME}" - -# Create X.509 provider -gcloud iam workload-identity-pools providers create-x509 "${PROVIDER_ID}" \ - --project="${PROJECT_ID}" \ - --location=global \ - --workload-identity-pool="${POOL_ID}" \ - --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="${PROJECT_ID}" \ - --display-name="SA Test" - -# Allow impersonation of the service account -gcloud iam service-accounts add-iam-policy-binding \ - "${SA_EMAIL}" \ - --role=roles/iam.workloadIdentityUser \ - --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/subject/test-client" - -# Grant the service account read access to the project -gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ - --member="serviceAccount:${SA_EMAIL}" \ - --role=roles/browser - -# Create credentials configuration files -gcloud iam workload-identity-pools create-cred-config \ - "projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}" \ - --service-account="${SA_EMAIL}" \ - --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://${BUCKET}" \ - --project="${PROJECT_ID}" \ - --location="${BUCKET_LOCATION}" \ - --uniform-bucket-level-access - -# Allow the service account to read and write objects in the bucket -gcloud storage buckets add-iam-policy-binding "gs://${BUCKET}" \ - --member="serviceAccount:${SA_EMAIL}" \ - --role=roles/storage.objectUser - -# Enable Data Access audit logs for Cloud Storage (project-wide, not per bucket) -gcloud projects get-iam-policy "${PROJECT_ID}" --format=yaml > iam_policy.yaml -node add_audit_config.mjs -gcloud projects set-iam-policy "${PROJECT_ID}" 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 -gcloud config set project "${PROJECT_ID}" - -# Test the authentication by listing the project details -gcloud auth list -gcloud projects describe "${PROJECT_ID}" -gcloud projects get-iam-policy "${PROJECT_ID}" \ - --flatten="bindings[].members" \ - --format="table(bindings.role, bindings.members)" - -# Test the bucket access by uploading and downloading a file -gcloud storage ls "gs://${BUCKET}" -gcloud storage cp README.md "gs://${BUCKET}/README.md" -gcloud storage ls "gs://${BUCKET}" -gcloud storage cp "gs://${BUCKET}/README.md" README.downloaded.md -cmp README.md README.downloaded.md - -# Then, switch back to your original account -gcloud config configurations activate default - -# Create a managed folder and make the objects in it publicly readable -gcloud storage managed-folders create "gs://${BUCKET}/public/" -gcloud storage managed-folders add-iam-policy-binding "gs://${BUCKET}/public/" \ - --member=allUsers \ - --role=roles/storage.objectViewer - -# List the audit log of the operations made by the service account -gcloud logging read \ - "protoPayload.authenticationInfo.principalEmail=\"${SA_EMAIL}\"" \ - --project="${PROJECT_ID}" \ - --freshness=1h \ - --order=asc \ - --format="table(timestamp, protoPayload.serviceName, protoPayload.methodName, protoPayload.authenticationInfo.principalEmail:label=EMAIL, protoPayload.authenticationInfo.principalSubject:label=SUBJECT, protoPayload.authenticationInfo.serviceAccountDelegationInfo[0].principalSubject:label=DELEGATED_BY)" - -# and again switch back to the service account -gcloud config configurations activate wif-test +bash make_wif.sh ``` +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. + 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 diff --git a/make_wif.sh b/make_wif.sh index 29ff4f2..a190d01 100755 --- a/make_wif.sh +++ b/make_wif.sh @@ -94,5 +94,5 @@ gcloud logging read \ --order=asc \ --format="table(timestamp, protoPayload.serviceName, protoPayload.methodName, protoPayload.authenticationInfo.principalEmail:label=EMAIL, protoPayload.authenticationInfo.principalSubject:label=SUBJECT, protoPayload.authenticationInfo.serviceAccountDelegationInfo[0].principalSubject:label=DELEGATED_BY)" -# and again switch back to the service account -gcloud config configurations activate wif-test +# optionally switch back to the service account for further testing +# gcloud config configurations activate wif-test