From 89b7681628b3823c5b7338461fa1ea2f2a4a6d90 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sat, 17 Oct 2026 17:36:19 +0200 Subject: [PATCH] Enhanced Vault guides and fixed some wording. --- README.md | 5 +++- docs/vault-approle.md | 18 +++++++++++++ docs/vault-entra-id.md | 21 ++++++++++++++- docs/vault-userpass.md | 39 +++++++++++++++++++-------- package.json | 3 +++ vault_create_user.mjs | 60 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 vault_create_user.mjs diff --git a/README.md b/README.md index ef9e20a..a45a1ff 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This short guide provides step by step instructions how to replace a JSON key wi - `openssl` 3.x - `vault` (only for the Vault based CA in step 1a) -- Node.js with `npm` +- Node.js 24 or later with `npm` - `gcloud` authenticated as an administrator of the target project ## Configuration @@ -150,6 +150,9 @@ Requesting the client certificate from a remote Vault server with a user account ```json { + "engines": { + "node": ">=24" + }, "dependencies": { "yaml": ">=2.9.1" } diff --git a/docs/vault-approle.md b/docs/vault-approle.md index 1b0ea40..ab2d9fe 100644 --- a/docs/vault-approle.md +++ b/docs/vault-approle.md @@ -291,3 +291,21 @@ Role: **Server administrator** ``` The service logs to the journal. A run that finds a valid certificate logs `nothing to do`, a renewal logs the new expiry date and the hook. + +## Next steps + +1. Create the credentials configuration `cred.json` on the server for the certificate and the key managed by the renewal service. The names come from `env.sh`. + + ```bash + 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=/var/lib/vault-cert/client_cert.pem \ + --credential-cert-private-key-path=/var/lib/vault-cert/client_key.pem \ + --output-file=cred.json + ``` + +2. Activate the service account with `gcloud auth login --cred-file=cred.json`. +3. Run the WIF tests. + +Example test commands are in `test_wif.sh`. diff --git a/docs/vault-entra-id.md b/docs/vault-entra-id.md index 801ee2a..e3fa22c 100644 --- a/docs/vault-entra-id.md +++ b/docs/vault-entra-id.md @@ -102,9 +102,10 @@ Role: **User** export VAULT_CACERT=/path/to/server-ca.pem ``` -2. Log in with the Entra ID account. The CLI starts a listener on `localhost:8250` and opens the browser with the Entra ID sign-in page. After the sign-in the CLI stores the token in the token helper file (`~/.vault-token`). +2. Unset `VAULT_TOKEN`, because a token in this environment variable takes precedence over the token stored by `vault login` and the CLI would keep using it. Then log in with the Entra ID account. The CLI starts a listener on `localhost:8250` and opens the browser with the Entra ID sign-in page. After the sign-in the CLI stores the token in the token helper file (`~/.vault-token`). ```bash + unset VAULT_TOKEN vault login -method=oidc role=entra-pki ``` @@ -141,3 +142,21 @@ Role: **User** ``` The user token can only sign with the `wif-client` role. Reading the role or any other path returns `permission denied`. + +## Next steps + +1. Create the credentials configuration `cred.json` for `client_cert.pem` and `client_key.pem`. The names come from `env.sh`. + + ```bash + 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 + ``` + +2. Activate the service account with `gcloud auth login --cred-file=cred.json`. +3. Run the WIF tests. + +Example test commands are in `test_wif.sh`. diff --git a/docs/vault-userpass.md b/docs/vault-userpass.md index 6e2d453..60dd983 100644 --- a/docs/vault-userpass.md +++ b/docs/vault-userpass.md @@ -6,7 +6,7 @@ This guide shows how an administrator configures the Vault userpass authenticati - The Vault PKI configured as described in step 1a of the main `README.md`: the `pki_int` mount and the `wif-client` role (`vault_configure.sh`). - A Vault server reachable over HTTPS. The examples use `https://vault.example.com:8200`. -- `vault` and `openssl` on the workstation of the administrator and of the user. +- `vault` and `openssl` on the workstation of the administrator and of the user, and Node.js 24 or later on the workstation of the administrator. The commands are written for bash. @@ -23,7 +23,7 @@ export VAULT_TOKEN= 1. Create a policy that allows signing certificate requests with the `wif-client` role and nothing else. 2. Enable the userpass authentication method. -3. Create the user and attach the policy. The token of the user is valid for 15 minutes and can be renewed up to 1 hour. +3. Create the user with `vault_create_user.mjs` and attach the policy. The token of the user is valid for 15 minutes and can be renewed up to 1 hour. `pki-client.hcl`: @@ -38,16 +38,14 @@ vault policy write pki-client pki-client.hcl vault auth enable userpass -read -r -s -p "Password: " PASSWORD; echo -printf '%s' "${PASSWORD}" | vault write auth/userpass/users/alice \ - password=- \ - token_policies=pki-client \ - token_ttl=15m \ - token_max_ttl=1h -unset PASSWORD +node vault_create_user.mjs --policy pki-client alice ``` -`password=-` makes the CLI read the password from standard input, so it does not appear in the process list or in the shell history. +`vault_create_user.mjs` takes the user name and one or more `--policy` options, one for each policy. The optional `--token-ttl` (default `15m`) and `--token-max-ttl` (default `1h`) options set the lifetime of the tokens of the user. It asks for the password twice without echoing it and creates the user through the Vault HTTP API with the `VAULT_ADDR` and `VAULT_TOKEN` of the administrator. The password never appears in the process list or in the shell history. It needs only Node.js 24 or later, no npm packages. When the server certificate is issued by a private CA, point Node.js at the CA certificate: + +```bash +export NODE_EXTRA_CA_CERTS=/path/to/server-ca.pem +``` > **Note:** The `wif-client` role accepts any common name (`allow_any_name=true`). Every user with the `pki-client` policy can therefore request a certificate with any subject CN, including the CN that other users or workloads rely on as the WiF subject. Restrict the role (`allowed_domains`, `allow_bare_domains`, `enforce_hostnames`) when the users must not choose their CN freely. @@ -62,9 +60,10 @@ Role: **User** export VAULT_CACERT=/path/to/server-ca.pem ``` -2. Log in. The CLI asks for the password and stores the token in the token helper file (`~/.vault-token`). +2. Unset `VAULT_TOKEN`, because a token in this environment variable takes precedence over the token stored by `vault login` and the CLI would keep using it. Then log in. The CLI asks for the password and stores the token in the token helper file (`~/.vault-token`). ```bash + unset VAULT_TOKEN vault login -method=userpass username=alice ``` @@ -107,3 +106,21 @@ To avoid storing the token on disk, log in with `-no-store -token-only` and keep ```bash export VAULT_TOKEN=$(vault login -no-store -token-only -method=userpass username=alice) ``` + +## Next steps + +1. Create the credentials configuration `cred.json` for `client_cert.pem` and `client_key.pem`. The names come from `env.sh`. + + ```bash + 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 + ``` + +2. Activate the service account with `gcloud auth login --cred-file=cred.json`. +3. Run the WIF tests. + +Example test commands are in `test_wif.sh`. diff --git a/package.json b/package.json index 65ba123..80cc69b 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,7 @@ { + "engines": { + "node": ">=24" + }, "dependencies": { "yaml": ">=2.9.1" } diff --git a/vault_create_user.mjs b/vault_create_user.mjs new file mode 100644 index 0000000..c761af3 --- /dev/null +++ b/vault_create_user.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +import { createInterface } from "node:readline"; +import { parseArgs } from "node:util"; + +const { values, positionals } = parseArgs({ + allowPositionals: true, + options: { + policy: { type: "string", multiple: true, default: [] }, + "token-ttl": { type: "string", default: "15m" }, + "token-max-ttl": { type: "string", default: "1h" }, + }, +}); +const [username] = positionals; +const policies = values.policy; +const address = process.env.VAULT_ADDR; +const token = process.env.VAULT_TOKEN; + +if (!username || policies.length === 0 || !address || !token) { + console.error( + "Usage: VAULT_ADDR=... VAULT_TOKEN=... vault_create_user.mjs --policy POLICY [--policy POLICY...] [--token-ttl TTL] [--token-max-ttl TTL] USERNAME", + ); + process.exit(1); +} + +// Without an output stream readline does not echo the typed characters. +const rl = createInterface({ input: process.stdin, terminal: process.stdin.isTTY }); +const lines = rl[Symbol.asyncIterator](); + +const ask = async (prompt) => { + process.stdout.write(prompt); + const { value } = await lines.next(); + process.stdout.write("\n"); + return value ?? ""; +}; + +const password = await ask("Password: "); +const confirmation = await ask("Repeat the password: "); +rl.close(); + +if (password === "" || password !== confirmation) { + console.error("The passwords are empty or do not match"); + process.exit(1); +} + +const response = await fetch(`${address}/v1/auth/userpass/users/${username}`, { + method: "POST", + headers: { "X-Vault-Token": token, "Content-Type": "application/json" }, + body: JSON.stringify({ + password, + token_policies: policies, + token_ttl: values["token-ttl"], + token_max_ttl: values["token-max-ttl"], + }), +}); + +if (!response.ok) { + console.error(`Vault returned ${response.status}: ${await response.text()}`); + process.exit(1); +} +console.log(`The user ${username} was created with the policies: ${policies.join(", ")}`);