feat(actions): configurable SSH user + insecure-TLS for PVE; real action whitelist + Z wrapper
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,21 @@
|
||||
{ "hosts": {}, "actions": [] }
|
||||
{
|
||||
"hosts": { "z": "192.168.1.124" },
|
||||
"actions": [
|
||||
{ "id": "restart-pihole", "label": "Restart Pi-hole (CT 106)", "kind": "service_restart", "host": "z", "service": "pihole-FTL", "tier": "safe" },
|
||||
{ "id": "restart-gitea", "label": "Restart Gitea (CT 105)", "kind": "service_restart", "host": "z", "service": "gitea", "tier": "safe" },
|
||||
{ "id": "restart-n8n", "label": "Restart n8n (CT 110)", "kind": "service_restart", "host": "z", "service": "n8n", "tier": "safe" },
|
||||
{ "id": "restart-magicmirror", "label": "Restart MagicMirror (CT 111)", "kind": "service_restart", "host": "z", "service": "magicmirror", "tier": "safe" },
|
||||
|
||||
{ "id": "start-ct107", "label": "Start iVentoy (CT 107)", "kind": "guest_power", "node": "z", "vmid": 107, "kindPath": "lxc", "op": "start", "tier": "safe" },
|
||||
{ "id": "stop-ct107", "label": "Stop iVentoy (CT 107)", "kind": "guest_power", "node": "z", "vmid": 107, "kindPath": "lxc", "op": "stop", "tier": "risky" },
|
||||
|
||||
{ "id": "start-ct110", "label": "Start n8n (CT 110)", "kind": "guest_power", "node": "z", "vmid": 110, "kindPath": "lxc", "op": "start", "tier": "safe" },
|
||||
{ "id": "stop-ct110", "label": "Stop n8n (CT 110)", "kind": "guest_power", "node": "z", "vmid": 110, "kindPath": "lxc", "op": "stop", "tier": "risky" },
|
||||
|
||||
{ "id": "start-ct111", "label": "Start MagicMirror (CT 111)", "kind": "guest_power", "node": "z", "vmid": 111, "kindPath": "lxc", "op": "start", "tier": "safe" },
|
||||
{ "id": "stop-ct111", "label": "Stop MagicMirror (CT 111)", "kind": "guest_power", "node": "z", "vmid": 111, "kindPath": "lxc", "op": "stop", "tier": "risky" },
|
||||
|
||||
{ "id": "start-vm200", "label": "Start OpenClaw (VM 200)", "kind": "guest_power", "node": "z", "vmid": 200, "kindPath": "qemu", "op": "start", "tier": "safe" },
|
||||
{ "id": "stop-vm200", "label": "Stop OpenClaw (VM 200)", "kind": "guest_power", "node": "z", "vmid": 200, "kindPath": "qemu", "op": "stop", "tier": "risky" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Forced command for the Void's restricted key. Maps a whitelisted action id to a
|
||||
# concrete systemctl restart. The id arrives via SSH_ORIGINAL_COMMAND; nothing else
|
||||
# is honoured. Edit the case list per host. Keep in sync with config/actions.json.
|
||||
# Forced command for the Void's restricted key on Z (installed in root's
|
||||
# authorized_keys via command="..."). Maps a whitelisted service-restart action
|
||||
# id to a FIXED `pct exec ... systemctl restart`. The id arrives via
|
||||
# SSH_ORIGINAL_COMMAND; nothing else is honoured — no input is interpolated into a
|
||||
# command. Guest power goes through the Proxmox API, NOT this wrapper. Keep the
|
||||
# case list in sync with config/actions.json (service_restart entries).
|
||||
#
|
||||
# Install on each target host:
|
||||
# Install on Z:
|
||||
# install -m 755 void-act /usr/local/bin/void-act
|
||||
# # in voidact's ~/.ssh/authorized_keys, prefix the Void's pubkey with:
|
||||
# command="/usr/local/bin/void-act",no-port-forwarding,no-pty,no-X11-forwarding <pubkey>
|
||||
# # prefix the Void's pubkey in /root/.ssh/authorized_keys with:
|
||||
# command="/usr/local/bin/void-act",no-port-forwarding,no-pty,no-X11-forwarding,no-agent-forwarding <pubkey>
|
||||
set -euo pipefail
|
||||
id="${SSH_ORIGINAL_COMMAND:-}"
|
||||
case "$id" in
|
||||
restart-caddy-ct100) exec systemctl restart caddy ;;
|
||||
*) echo "void-act: refused '$id'" >&2; exit 13 ;;
|
||||
case "${SSH_ORIGINAL_COMMAND:-}" in
|
||||
restart-pihole) exec pct exec 106 -- systemctl restart pihole-FTL ;;
|
||||
restart-gitea) exec pct exec 105 -- systemctl restart gitea ;;
|
||||
restart-n8n) exec pct exec 110 -- systemctl restart n8n ;;
|
||||
restart-magicmirror) exec pct exec 111 -- systemctl restart magicmirror ;;
|
||||
*) echo "void-act: refused '${SSH_ORIGINAL_COMMAND:-}'" >&2; exit 13 ;;
|
||||
esac
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import { Agent } from 'undici';
|
||||
|
||||
// PVE uses a self-signed cert on the LAN; opt into skipping verification with
|
||||
// PROXMOX_INSECURE_TLS=1 (homelab). Cached dispatcher; tests inject fetchImpl.
|
||||
let insecure;
|
||||
function tlsDispatcher() {
|
||||
if (process.env.PROXMOX_INSECURE_TLS !== '1') return undefined;
|
||||
insecure ??= new Agent({ connect: { rejectUnauthorized: false } });
|
||||
return insecure;
|
||||
}
|
||||
|
||||
// Proxmox guest power via a SCOPED PVEAPIToken (VM.PowerMgmt on whitelisted guests
|
||||
// only). PVE enforces permissions server-side; this adapter never builds shell commands.
|
||||
export async function powerGuest({ node, vmid, op, kindPath = 'lxc' }, {
|
||||
@@ -8,7 +19,8 @@ export async function powerGuest({ node, vmid, op, kindPath = 'lxc' }, {
|
||||
const url = `${apiUrl}/api2/json/nodes/${node}/${kindPath}/${vmid}/status/${op}`;
|
||||
const res = await fetchImpl(url, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `PVEAPIToken=${token}` }
|
||||
headers: { Authorization: `PVEAPIToken=${token}` },
|
||||
dispatcher: tlsDispatcher()
|
||||
});
|
||||
if (!res.ok) throw new Error(`proxmox ${op} ${vmid} → ${res.status} ${await res.text?.() ?? ''}`);
|
||||
const body = await res.json();
|
||||
|
||||
@@ -7,7 +7,7 @@ const ID_RE = /^[a-z0-9-]+$/;
|
||||
// its OWN whitelist. We pass ONLY the id as a single argv element — no shell.
|
||||
export function restartService({ ip, actionId }, {
|
||||
keyPath = process.env.ACTIONS_SSH_KEY,
|
||||
user = 'voidact',
|
||||
user = process.env.ACTIONS_SSH_USER || 'voidact',
|
||||
spawnImpl = nodeSpawn
|
||||
} = {}) {
|
||||
if (!ID_RE.test(actionId || '')) return Promise.reject(new Error(`invalid action id: ${actionId}`));
|
||||
|
||||
Reference in New Issue
Block a user