#!/bin/sh
#
# pmx-pve-audit
#
# Restricted read-only command wrapper for Proxmox-node audit access.
#
# Intended deployment:
# - install manually on Proxmox nodes as `/root/bin/pmx-pve-audit`
# - reference it from `~/.ssh/authorized_keys` via a forced-command entry
# no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding,command="/root/bin/pmx-pve-audit" ssh-ed25519 <key> [comment]
# - run it either as:
# pmx-pve-audit <command> [args]
# or, over SSH forced-command, as:
# <command> [args]
#
# This wrapper intentionally accepts only a narrow allowlist of exact command
# forms. Unsupported commands or unsupported argument shapes fail closed and
# print the supported forms.
set -eu
set -f
PATH='/usr/sbin:/usr/bin:/sbin:/bin'
g_validate_only=0
fn_usage() {
cat <<'EOF'
This is a restricted command wrapper.
Supported commands:
cat /proc/pressure/cpu [/proc/pressure/memory] [/proc/pressure/io]
pct df <vmid>
ssh <host> <supported-command...>
smartctl --scan-open
smartctl -i -H -j <device>
zfs get -Hp -o value used <dataset>
zpool status -p
pvecm status
EOF
}
fn_version() {
printf 'pmx-pve-audit v1\n'
exit 0
}
fn_is_pressure_path() {
case "$1" in
/proc/pressure/cpu | /proc/pressure/memory | /proc/pressure/io)
return 0
;;
*)
return 1
;;
esac
}
fn_validate_vmid() {
printf '%s\n' "$1" | grep -Eq '^[0-9]+$'
}
fn_validate_device() {
case "$1" in
/dev/*)
;;
*)
return 1
;;
esac
case "$1" in
*' '* | *'..'* | *'//'*)
return 1
;;
esac
printf '%s\n' "$1" | grep -Eq '^[A-Za-z0-9_./:-]+$'
}
fn_validate_dataset() {
case "$1" in
'' | /* | *' '* | *'..'* | *'//'*)
return 1
;;
esac
case "$1" in
*/*)
;;
*)
return 1
;;
esac
printf '%s\n' "$1" | grep -Eq '^[A-Za-z0-9_./:-]+$'
}
fn_validate_host() {
case "$1" in
'' | -* | *'@'* | *' '* | *'..'* | *'//'*)
return 1
;;
esac
printf '%s\n' "$1" | grep -Eq '^[A-Za-z0-9.-]+$'
}
fn_require_cluster_host() {
if ! command -v pvesh >/dev/null 2>&1; then
return 1
fi
if pvesh get /nodes --output-format json 2>/dev/null |
grep -o '"node":"[^"]*"' |
sed 's/"node":"//; s/"$//' |
grep -Fx "$1" >/dev/null 2>&1; then
return 0
fi
return 1
}
fn_shell_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\"'\"'/g")"
}
fn_build_remote_command() {
out=""
for b_arg in "$@"; do
if test -z "$out"; then
out=$(fn_shell_quote "$b_arg")
else
out="$out $(fn_shell_quote "$b_arg")"
fi
done
printf '%s\n' "$out"
}
fn_finish_validate() {
if test "$g_validate_only" = "1"; then
return 0
fi
return 1
}
# AGENT: Keep this function, fn_is_pressure_path, and the exact "cat ..." line
# in fn_usage in sync. If the allowed pressure-file list changes, update all
# three places together so agents get exact feedback from the wrapper.
fn_cat() {
if test $# -lt 1; then
fn_usage >&2
exit 1
fi
for b_arg in "$@"; do
if ! fn_is_pressure_path "$b_arg"; then
printf 'unsupported cat path: %s\n' "$b_arg" >&2
fn_usage >&2
exit 1
fi
done
if fn_finish_validate; then
return 0
fi
exec cat "$@"
}
fn_pct() {
if test $# -ne 2; then
fn_usage >&2
exit 1
fi
if test "$1" != "df"; then
printf 'unsupported pct subcommand: %s\n' "$1" >&2
fn_usage >&2
exit 1
fi
if ! fn_validate_vmid "$2"; then
printf 'invalid vmid: %s\n' "$2" >&2
fn_usage >&2
exit 1
fi
if fn_finish_validate; then
return 0
fi
exec pct df "$2"
}
fn_smartctl() {
case "${1:-}" in
--scan-open)
if test $# -ne 1; then
fn_usage >&2
exit 1
fi
exec smartctl --scan-open
;;
-i)
if test $# -ne 4; then
fn_usage >&2
exit 1
fi
if test "$2" != "-H"; then
fn_usage >&2
exit 1
fi
if test "$3" != "-j"; then
fn_usage >&2
exit 1
fi
if ! fn_validate_device "$4"; then
printf 'invalid device path: %s\n' "$4" >&2
fn_usage >&2
exit 1
fi
if fn_finish_validate; then
return 0
fi
exec smartctl -i -H -j "$4"
;;
*)
printf 'unsupported smartctl invocation\n' >&2
fn_usage >&2
exit 1
;;
esac
}
fn_zfs() {
if test $# -ne 6; then
fn_usage >&2
exit 1
fi
if test "$1" != "get"; then
fn_usage >&2
exit 1
fi
if test "$2" != "-Hp"; then
fn_usage >&2
exit 1
fi
if test "$3" != "-o"; then
fn_usage >&2
exit 1
fi
if test "$4" != "value"; then
fn_usage >&2
exit 1
fi
if test "$5" != "used"; then
fn_usage >&2
exit 1
fi
if ! fn_validate_dataset "$6"; then
printf 'invalid dataset: %s\n' "$6" >&2
fn_usage >&2
exit 1
fi
if fn_finish_validate; then
return 0
fi
exec /sbin/zfs get -Hp -o value used "$6"
}
fn_zpool() {
if test $# -ne 2; then
fn_usage >&2
exit 1
fi
if test "$1" != "status"; then
fn_usage >&2
exit 1
fi
if test "$2" != "-p"; then
fn_usage >&2
exit 1
fi
if fn_finish_validate; then
return 0
fi
exec /sbin/zpool status -p
}
fn_pvecm() {
if test $# -ne 1; then
fn_usage >&2
exit 1
fi
if test "$1" != "status"; then
fn_usage >&2
exit 1
fi
if fn_finish_validate; then
return 0
fi
exec pvecm status
}
fn_dispatch() {
case "${1:-}" in
cat)
shift
fn_cat "$@"
;;
pct)
shift
fn_pct "$@"
;;
smartctl)
shift
fn_smartctl "$@"
;;
zfs)
shift
fn_zfs "$@"
;;
zpool)
shift
fn_zpool "$@"
;;
pvecm)
shift
fn_pvecm "$@"
;;
help | -help | --help)
if test "$g_validate_only" = "1"; then
return 0
fi
fn_usage
exit 0
;;
version | -V | -version | --version)
if test "$g_validate_only" = "1"; then
return 0
fi
fn_version
;;
*)
printf 'unsupported command: %s\n' "${1:-}" >&2
fn_usage >&2
exit 1
;;
esac
}
fn_execute_args() {
remote_host=""
if test "${1:-}" = "pmx-pve-audit"; then
shift
fi
if test $# -eq 0; then
fn_usage
exit 0
fi
if test "${1:-}" = "ssh"; then
shift
if test $# -lt 2; then
fn_usage >&2
exit 1
fi
remote_host=$1
shift
if ! fn_validate_host "$remote_host"; then
printf 'invalid ssh host: %s\n' "$remote_host" >&2
fn_usage >&2
exit 1
fi
if ! fn_require_cluster_host "$remote_host"; then
printf 'unknown cluster host: %s\n' "$remote_host" >&2
fn_usage >&2
exit 1
fi
fi
b_prev_validate_only=$g_validate_only
g_validate_only=1
fn_dispatch "$@"
g_validate_only=$b_prev_validate_only
if test -n "$remote_host"; then
remote_command=$(fn_build_remote_command "$@")
exec ssh \
-o BatchMode=yes \
"$remote_host" \
"$remote_command"
fi
fn_dispatch "$@"
}
if test $# -gt 0; then
fn_execute_args "$@"
fi
b_original=${SSH_ORIGINAL_COMMAND:-}
if test -z "$b_original"; then
fn_usage >&2
exit 1
fi
# shellcheck disable=SC2086
set -- $b_original
fn_execute_args "$@"