--- name: auth-proxmox-api version: 1.0.0 description: Set up Proxmox VE API auth, TLS, base curl config, and task UPID polling tags: [proxmox, auth, api, shell] --- # Auth Proxmox API ## Pass-Through Design The token is always caller-supplied. Tools never hold stored credentials or substitute a higher-privilege token when a caller's token is insufficient. If the Proxmox API returns 403, surface it to the caller — do not retry with elevated credentials. For local CLI use, load from `.env` / `.env.secret` as a developer convenience. For service/API contexts, require the caller to inject `PROXMOX_TOKEN_ID` and `PROXMOX_TOKEN_SECRET` into ENV directly — do not source credential files. For minimum required permissions per operation, see `docs/token-permissions.md`. ## Auth Header ```sh my_auth="Authorization: PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_TOKEN_SECRET}" my_base_url="https://${PROXMOX_HOST}/api2/json" my_curl="curl -k --max-time 15 --fail-with-body -sSL" ``` - `PROXMOX_TOKEN_ID` format: `user@realm!tokenname` (e.g. `john@pve!mytoken`) - `PROXMOX_TOKEN_SECRET`: UUID (e.g. `00000000-0000-4000-8000-000000000000`) - `-k` skips TLS verify — Proxmox is commonly self-signed - `--fail-with-body` returns error body instead of swallowing it ## Load ENV **CLI / local use** — load from files as developer convenience: ```sh if test -e .env; then # shellcheck disable=SC1091 . ./.env fi if test -e .env.secret; then # shellcheck disable=SC1091 . ./.env.secret || true fi ``` **Service / API context** — caller injects token into ENV directly; do not source files. Either way, validate before proceeding — fail if token is absent rather than silently using a wrong or missing credential: ```sh if test -z "${PROXMOX_HOST:-}" || test -z "${PROXMOX_TOKEN_ID:-}" || test -z "${PROXMOX_TOKEN_SECRET:-}" || test -z "${PROXMOX_TARGET_NODE:-}"; then echo "Error: PROXMOX_TOKEN_ID and PROXMOX_TOKEN_SECRET must be set by caller" >&2 exit 1 fi ``` ## Making Requests ```sh # GET ${my_curl} -H "${my_auth}" "${my_base_url}/nodes" # POST with form data ${my_curl} -H "${my_auth}" \ -X POST "${my_base_url}/nodes/${node}/lxc" \ --data-urlencode "hostname=${hostname}" \ --data-urlencode "memory=${memory}" # DELETE ${my_curl} -H "${my_auth}" \ -X DELETE "${my_base_url}/nodes/${node}/lxc/${vmid}?purge=1" ``` ## Task UPID Polling Write operations return a UPID string: `UPID:pve1:000C38ED:01978844:...:vzcreate:101:root@pam:` Poll status with the URL-percent-encoded UPID: ```sh fn_wait_status() { ( my_target_node="${1}" my_task_id_raw="${2}" my_check_count="${3:-1}" my_task_id="$( printf '%s' "${my_task_id_raw}" | xxd -plain | tr -d '\n' | sed 's/\(..\)/%\1/g' )" my_task_result="$( ${my_curl} -H "${my_auth}" \ "${my_base_url}/nodes/${my_target_node}/tasks/${my_task_id}/status" )" my_task_status="$(printf '%s' "${my_task_result}" | jq -r '.data.status')" my_exit_status="$(printf '%s' "${my_task_result}" | jq -r '.data.exitstatus')" if test "$my_exit_status" != "null" && test "$my_exit_status" != "OK"; then echo "ERROR: ${my_exit_status}" >&2 return 1 fi if test "${my_task_status}" = "running"; then if test "${my_check_count}" -ge 30; then echo "Error: timed out after 30 checks" >&2 return 1 fi sleep 1 fn_wait_status "${my_target_node}" "${my_task_id_raw}" "$((my_check_count + 1))" return 0 fi printf '%s' "${my_task_result}" | jq '.' ); } ``` ## Cluster Discovery Endpoints ```sh GET /api2/json/nodes # list nodes GET /api2/json/cluster/resources # all VMs, CTs, storage, nodes GET /api2/json/cluster/nextid # next available VMID ``` ## Error Handling Proxmox error responses include the specific permission or message that failed. Always extract and surface it — never swallow the detail. ```sh fn_check_response() { ( my_result="${1}" my_status="$(printf '%s' "${my_result}" | jq -r '.status // empty' 2>/dev/null)" case "${my_status}" in 200|'') return 0 ;; 401) my_msg="$(printf '%s' "${my_result}" | jq -r '.message // "invalid token"')" printf 'Error 401 (authentication): %s\nToken: %s\n' \ "${my_msg}" "${PROXMOX_TOKEN_ID}" >&2 return 1 ;; 403) # Proxmox puts the specific privilege in .errors.permission, e.g.: # "Permission check failed (need ['perm', '/vms/100', ['VM.Allocate']])" my_msg="$(printf '%s' "${my_result}" | jq -r ' .errors.permission // (.errors | to_entries | .[0].value) // .message // "permission denied (no detail returned)" ')" printf 'Error 403 (permission denied): %s\nToken: %s\n' \ "${my_msg}" "${PROXMOX_TOKEN_ID}" >&2 return 1 ;; *) my_msg="$(printf '%s' "${my_result}" | jq -r '.message // .errors // "unknown error"')" printf 'Error %s: %s\n' "${my_status}" "${my_msg}" >&2 return 1 ;; esac ); } ``` Use after every API call: ```sh my_result="$(${my_curl} -H "${my_auth}" "${my_base_url}/nodes")" fn_check_response "${my_result}" || return 1 printf '%s' "${my_result}" | jq -r '.data[].node' ``` Never retry with a higher-privilege token on 403. Caller must supply a correctly scoped token. ## Required ENV Vars ``` PROXMOX_HOST # host:port, e.g. 192.168.0.10:8006 PROXMOX_TOKEN_ID # user@realm!tokenname (caller-supplied) PROXMOX_TOKEN_SECRET # UUID token secret (caller-supplied) PROXMOX_TARGET_NODE # e.g. pve1 ```