Fixes for the App Role guide.

This commit is contained in:
slawek committed 2026-10-18 13:06:20 +02:00
1 parent 50fc51f734
commit c21ebd7725
5 files changed
+80 -150

No files matched your search

+45 -135
View File
@@ -1,11 +1,11 @@
# Automatic renewal of a client certificate on Ubuntu with Vault AppRole
This guide shows how an unattended Ubuntu server gets a client certificate from a remote Vault server with the AppRole authentication method, and keeps it fresh. A systemd timer starts a renewal service, the service renews the certificate before it expires, and after a successful renewal it runs a configurable Bash hook that signals the application to reread the certificate or restarts it. The server talks to Vault either with the Vault CLI (section 2) or with Python 3 through the Vault HTTP API (section 2a).
This guide shows how an unattended Ubuntu server gets a client certificate from a remote Vault server with the AppRole authentication method, and keeps it fresh. A systemd timer starts a renewal service, the service renews the certificate before it expires, and after a successful renewal it runs a configurable Bash hook that signals the application to reread the certificate or restarts it. The renewal script is written in Python 3 and talks to Vault through the Vault HTTP API, so the server does not need the Vault CLI.
## Prerequisites
- 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`.
- A Vault server reachable over HTTPS. The examples use `https://vault.koszewscy.waw.pl`.
- Ubuntu Server 24.04 LTS or 26.04 LTS with systemd and `sudo` access.
- The IP address of the server as seen by Vault. The examples use `203.0.113.10`.
@@ -18,15 +18,16 @@ Role: **Administrator**
Point the CLI at the server and authenticate with an administrator token:
```bash
export VAULT_ADDR=https://vault.example.com:8200
export VAULT_ADDR=https://vault.koszewscy.waw.pl
export VAULT_TOKEN=<admin-token>
```
1. Create a policy that allows signing certificate requests with the `wif-client` role and nothing else. Skip this step when the policy already exists.
2. Enable the AppRole authentication method.
3. Create the `cert-renew` role. It issues batch tokens valid for 10 minutes, the secret ID does not expire so that the server can renew unattended, and both the secret ID and the tokens work only from the IP address of the server.
4. Read the role ID.
5. Generate a response-wrapped secret ID that can be unwrapped once within 10 minutes. Deliver the role ID and the wrapping token to the owner of the server over two different channels.
3. Create the `cert-renew-<server_name>` role. It issues batch tokens valid for 10 minutes, the secret ID does not expire so that the server can renew unattended, and both the secret ID and the tokens work only from the IP address of the server.
4. Create the `cert-renew` role. It has the same settings, but the secret ID and the tokens work from any address.
5. Read the role ID of the role chosen for the server.
6. Generate a response-wrapped secret ID of that role that can be unwrapped once within 10 minutes. Deliver the role ID and the wrapping token to the owner of the server over two different channels.
`pki-client.hcl`:
@@ -41,7 +42,7 @@ vault policy write pki-client pki-client.hcl
vault auth enable approle
vault write auth/approle/role/cert-renew \
vault write auth/approle/role/cert-renew-<server_name> \
token_type=batch \
token_policies=pki-client \
token_ttl=10m \
@@ -50,12 +51,21 @@ vault write auth/approle/role/cert-renew \
secret_id_bound_cidrs=203.0.113.10/32 \
token_bound_cidrs=203.0.113.10/32
vault read -field=role_id auth/approle/role/cert-renew/role-id
vault write auth/approle/role/cert-renew \
token_type=batch \
token_policies=pki-client \
token_ttl=10m \
secret_id_ttl=0 \
secret_id_num_uses=0
vault read -field=role_id auth/approle/role/cert-renew-<server_name>/role-id
vault write -wrap-ttl=10m -f -field=wrapping_token \
auth/approle/role/cert-renew/secret-id
auth/approle/role/cert-renew-<server_name>/secret-id
```
The last two commands use the role of the server. Use `cert-renew` in their paths for a server that has no fixed address.
Generate a new secret ID, deliver it the same way and replace the file on the server periodically.
> **Note:** The `wif-client` role accepts any common name (`allow_any_name=true`). The server can therefore request a certificate with any subject CN, including the CN that other workloads rely on as the WiF subject. Restrict the role (`allowed_domains`, `allow_bare_domains`, `enforce_hostnames`) when the common name of the server must be fixed.
@@ -64,24 +74,21 @@ Generate a new secret ID, deliver it the same way and replace the file on the se
Role: **Server administrator**
### Install the Vault CLI
### Install the tools
The HashiCorp apt repository provides the `vault` package for Ubuntu 24.04 (`noble`) and 26.04 (`resolute`):
Python 3 handles the HTTP calls and the JSON answers of the Vault API with its standard library only, and `openssl` creates the key and the CSR. Python 3 is `python3` 3.12 on Ubuntu 24.04 and 3.14 on Ubuntu 26.04. The commands do nothing when the packages are already installed:
```bash
sudo apt update
sudo apt install -y wget gpg
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install -y vault
sudo apt install -y python3 openssl
```
### Store the credentials
1. Create the directories for the configuration and for the certificate.
2. Create the credential files with the access limited to root before writing into them.
3. Write the role ID, and unwrap the secret ID. The wrapping token can be unwrapped only once. When the unwrap fails, treat the wrapping token as compromised and ask for a new one.
3. Write the role ID.
4. Store the secret ID with the script. It asks for the wrapping token without echo, so the token does not appear in the process list or in the shell history. The configuration file is not read in this step, so pass the address of Vault. The wrapping token can be unwrapped only once. When the unwrap fails, treat the wrapping token as compromised and ask for a new one.
```bash
sudo install -d -m 0750 /etc/vault-cert /var/lib/vault-cert
@@ -89,15 +96,33 @@ sudo install -m 0600 /dev/null /etc/vault-cert/role_id
sudo install -m 0600 /dev/null /etc/vault-cert/secret_id
echo '<role-id>' | sudo tee /etc/vault-cert/role_id > /dev/null
VAULT_ADDR=https://vault.example.com:8200 vault unwrap -field=secret_id <wrapping-token> | sudo tee /etc/vault-cert/secret_id > /dev/null
sudo VAULT_ADDR=https://vault.koszewscy.waw.pl /usr/local/sbin/vault-cert-renew --unwrap
```
### Install the renewal script
The renewal script is `vault_cert_renew.py` in the repository. Install it at `/usr/local/sbin/vault-cert-renew`:
```bash
sudo install -m 0755 vault_cert_renew.py /usr/local/sbin/vault-cert-renew
```
The key is generated on the server and never leaves it. The script exits without contacting Vault while the certificate is valid for more than `RENEW_BEFORE_DAYS`, replaces the files only when the new certificate matches the new key, and runs the hook after a successful renewal. The Vault token stays in memory and the batch token expires on its own. A failed Vault call prints the status code and the answer of Vault to the journal.
Test the AppRole authentication with the `--login-test` option. The script logs in with the stored role ID and secret ID and prints the type, the policies and the lifetime of the token it received, without issuing a certificate. The configuration file is not read in this step, so pass the address of Vault. The expected result is a `batch` token with the `pki-client` policy valid for 600 seconds:
```bash
sudo VAULT_ADDR=https://vault.koszewscy.waw.pl /usr/local/sbin/vault-cert-renew --login-test
```
A `permission denied` answer means that the role ID or the secret ID is wrong, or that the request comes from an address outside the CIDR of the role.
### Configure the renewal
`/etc/vault-cert/vault-cert.env`:
```text
VAULT_ADDR=https://vault.example.com:8200
VAULT_ADDR=https://vault.koszewscy.waw.pl
CERT_CN=test-client
CERT_DIR=/var/lib/vault-cert
RENEW_BEFORE_DAYS=30
@@ -120,84 +145,6 @@ The settings are:
| `CERT_TTL` | `8760h` | Requested lifetime of the new certificate. |
| `HOOK` | none | Script run after a successful renewal. |
### Install the renewal script
`/usr/local/sbin/vault-cert-renew`, mode `0755`:
```bash
#!/bin/bash
set -euo pipefail
CERT_DIR="${CERT_DIR:-/var/lib/vault-cert}"
CERT_CN="${CERT_CN:?CERT_CN is not set}"
ROLE_ID_FILE="${ROLE_ID_FILE:-/etc/vault-cert/role_id}"
SECRET_ID_FILE="${SECRET_ID_FILE:-/etc/vault-cert/secret_id}"
SIGN_PATH="${SIGN_PATH:-pki_int/sign/wif-client}"
RENEW_BEFORE_DAYS="${RENEW_BEFORE_DAYS:-30}"
CERT_TTL="${CERT_TTL:-8760h}"
HOOK="${HOOK:-}"
cert="${CERT_DIR}/client_cert.pem"
key="${CERT_DIR}/client_key.pem"
# Do nothing while the certificate is valid for longer than the renewal window
if [ -f "${cert}" ]; then
if [ -f "${key}" ]; then
if openssl x509 -checkend $((RENEW_BEFORE_DAYS * 86400)) -noout -in "${cert}" > /dev/null; then
echo "The certificate is valid for more than ${RENEW_BEFORE_DAYS} days, nothing to do"
exit 0
fi
fi
fi
mkdir -p "${CERT_DIR}"
workdir="$(mktemp -d "${CERT_DIR}/.renew.XXXXXX")"
trap 'rm -rf "${workdir}"' EXIT
echo "Logging in to Vault with AppRole"
token="$(vault write -field=token auth/approle/login \
role_id=@"${ROLE_ID_FILE}" \
secret_id=@"${SECRET_ID_FILE}")"
echo "Generating the key and the CSR for ${CERT_CN}"
openssl genpkey \
-algorithm RSA \
-pkeyopt rsa_keygen_bits:4096 \
-out "${workdir}/client_key.pem"
openssl req \
-new \
-key "${workdir}/client_key.pem" \
-subj "/CN=${CERT_CN}" \
-out "${workdir}/client.csr"
echo "Signing the CSR"
VAULT_TOKEN="${token}" vault write -field=certificate "${SIGN_PATH}" \
csr=@"${workdir}/client.csr" \
common_name="${CERT_CN}" \
ttl="${CERT_TTL}" > "${workdir}/client_cert.pem"
# Replace the files only when the new certificate matches the new key
cert_pub="$(openssl x509 -in "${workdir}/client_cert.pem" -noout -pubkey)"
key_pub="$(openssl pkey -in "${workdir}/client_key.pem" -pubout)"
if [ "${cert_pub}" != "${key_pub}" ]; then
echo "The issued certificate does not match the generated key" >&2
exit 1
fi
chmod 0600 "${workdir}/client_key.pem"
chmod 0644 "${workdir}/client_cert.pem"
mv -f "${workdir}/client_key.pem" "${key}"
mv -f "${workdir}/client_cert.pem" "${cert}"
echo "The certificate was renewed, it expires on $(openssl x509 -in "${cert}" -noout -enddate | cut -d= -f2)"
if [ -n "${HOOK}" ]; then
echo "Running the hook ${HOOK}"
CERT_FILE="${cert}" KEY_FILE="${key}" "${HOOK}"
fi
```
The key is generated on the server and never leaves it. The script exits successfully without contacting Vault while the certificate is valid for more than `RENEW_BEFORE_DAYS`. The batch token of the login is not stored and expires on its own.
### Write the hook
The hook runs as root after the new files are in place. It gets the paths of the certificate and the key in the `CERT_FILE` and `KEY_FILE` environment variables. A failing hook fails the service, the failure is visible in the journal and in `systemctl status`.
@@ -213,7 +160,7 @@ install -m 0640 -o root -g myapp "${KEY_FILE}" /etc/myapp/client_key.pem
systemctl reload myapp.service
```
Replace `reload` with `restart` when the application cannot reread the certificate. Leave `HOOK` unset to renew without any action.
Replace `reload` with `restart` when the application cannot reread the certificate. Leave `HOOK` unset to renew without any action. When `HOOK` names a file that does not exist or is not executable, the renewal still succeeds and the script logs a warning and skips the hook.
> **Note:** The certificate is already renewed when the hook runs. When the hook fails, the next timer run finds a valid certificate and does not run the hook again. Fix the hook and run it by hand with the environment variables set, or remove `client_cert.pem` to force a new renewal.
@@ -260,43 +207,6 @@ sudo systemctl daemon-reload
sudo systemctl enable --now vault-cert-renew.timer
```
## 2a. Alternative: use the Vault HTTP API on the server
Role: **Server administrator**
The server does not need the Vault CLI. Python 3 handles the HTTP calls and the JSON answers of the Vault API with its standard library only, and `openssl` creates the key and the CSR. Python 3 is `python3` 3.12 on Ubuntu 24.04 and 3.14 on Ubuntu 26.04. The configuration file, the hook and the systemd units from section 2 stay the same. Only three parts of section 2 are replaced: the installation of the Vault CLI, the unwrapping of the secret ID and the renewal script.
### Install the tools
Use these commands instead of the installation of the Vault CLI. They do nothing when the packages are already installed:
```bash
sudo apt update
sudo apt install -y python3 openssl
```
### Install the renewal script
The renewal script is `vault_cert_renew.py` in the repository. Install it at the path that the systemd service uses, `/usr/local/sbin/vault-cert-renew`, so the service stays unchanged:
```bash
sudo install -m 0755 vault_cert_renew.py /usr/local/sbin/vault-cert-renew
```
It reads the same settings from the configuration file, and `VAULT_ADDR` is required. With the `--unwrap` option it also stores the secret ID from a response wrapping token, so the Vault CLI is not needed for that either.
The script has the same behavior as the CLI version: it exits without contacting Vault while the certificate is valid, replaces the files only when the new certificate matches the new key, and runs the hook after a successful renewal. The Vault token stays in memory and the batch token expires on its own. A failed Vault call prints the status code and the answer of Vault to the journal.
### Store the credentials
Create the directories and the credential files as in section 2 and write the role ID the same way. Then store the secret ID with the script. It asks for the wrapping token without echo, so the token does not appear in the process list or in the shell history. The configuration file is not read in this step, so pass the address of Vault:
```bash
sudo VAULT_ADDR=https://vault.example.com:8200 /usr/local/sbin/vault-cert-renew --unwrap
```
The wrapping token can be unwrapped only once, as with the CLI. The script writes the secret ID to `/etc/vault-cert/secret_id` with the access limited to root.
## 3. Verify and operate
Role: **Server administrator**
+5 -5
View File
@@ -5,7 +5,7 @@ This guide shows how an administrator configures the Vault OIDC authentication m
## Prerequisites
- 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`.
- A Vault server reachable over HTTPS. The examples use `https://vault.koszewscy.waw.pl`.
- Permissions to register applications in the Entra ID tenant.
- `vault` and `openssl` on the workstation of the administrator and of the user, and a web browser on the workstation of the user.
@@ -19,7 +19,7 @@ Role: **Administrator**
2. Add the redirect URIs of the type "Web":
- `http://localhost:8250/oidc/callback` for the CLI login.
- `https://vault.example.com:8200/ui/vault/auth/oidc/oidc/callback` for the Vault UI login.
- `https://vault.koszewscy.waw.pl/ui/vault/auth/oidc/oidc/callback` for the Vault UI login.
3. Record the **Application (client) ID** and the **Directory (tenant) ID**.
4. Under **Certificates & secrets**, create a client secret and record its value.
@@ -34,7 +34,7 @@ Role: **Administrator**
Point the CLI at the server and authenticate with an administrator token:
```bash
export VAULT_ADDR=https://vault.example.com:8200
export VAULT_ADDR=https://vault.koszewscy.waw.pl
export VAULT_TOKEN=<admin-token>
```
@@ -72,7 +72,7 @@ path "pki_int/sign/wif-client" {
"user_claim": "sub",
"allowed_redirect_uris": [
"http://localhost:8250/oidc/callback",
"https://vault.example.com:8200/ui/vault/auth/oidc/oidc/callback"
"https://vault.koszewscy.waw.pl/ui/vault/auth/oidc/oidc/callback"
],
"groups_claim": "groups",
"oidc_scopes": ["profile"],
@@ -108,7 +108,7 @@ Role: **User**
1. Set the address of the Vault server. Set `VAULT_CACERT` as well when the server certificate is issued by a private CA.
```bash
export VAULT_ADDR=https://vault.example.com:8200
export VAULT_ADDR=https://vault.koszewscy.waw.pl
export VAULT_CACERT=/path/to/server-ca.pem
```
+3 -3
View File
@@ -5,7 +5,7 @@ This guide shows how an administrator configures the Vault userpass authenticati
## Prerequisites
- 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`.
- A Vault server reachable over HTTPS. The examples use `https://vault.koszewscy.waw.pl`.
- `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.
@@ -17,7 +17,7 @@ Role: **Administrator**
Point the CLI at the server and authenticate with an administrator token:
```bash
export VAULT_ADDR=https://vault.example.com:8200
export VAULT_ADDR=https://vault.koszewscy.waw.pl
export VAULT_TOKEN=<admin-token>
```
@@ -56,7 +56,7 @@ Role: **User**
1. Set the address of the Vault server. Set `VAULT_CACERT` as well when the server certificate is issued by a private CA.
```bash
export VAULT_ADDR=https://vault.example.com:8200
export VAULT_ADDR=https://vault.koszewscy.waw.pl
export VAULT_CACERT=/path/to/server-ca.pem
```