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
|
#!/usr/bin/env bash
|
||||||
# Forced command for the Void's restricted key. Maps a whitelisted action id to a
|
# Forced command for the Void's restricted key on Z (installed in root's
|
||||||
# concrete systemctl restart. The id arrives via SSH_ORIGINAL_COMMAND; nothing else
|
# authorized_keys via command="..."). Maps a whitelisted service-restart action
|
||||||
# is honoured. Edit the case list per host. Keep in sync with config/actions.json.
|
# 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
|
# install -m 755 void-act /usr/local/bin/void-act
|
||||||
# # in voidact's ~/.ssh/authorized_keys, prefix the Void's pubkey with:
|
# # 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 <pubkey>
|
# command="/usr/local/bin/void-act",no-port-forwarding,no-pty,no-X11-forwarding,no-agent-forwarding <pubkey>
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
id="${SSH_ORIGINAL_COMMAND:-}"
|
case "${SSH_ORIGINAL_COMMAND:-}" in
|
||||||
case "$id" in
|
restart-pihole) exec pct exec 106 -- systemctl restart pihole-FTL ;;
|
||||||
restart-caddy-ct100) exec systemctl restart caddy ;;
|
restart-gitea) exec pct exec 105 -- systemctl restart gitea ;;
|
||||||
*) echo "void-act: refused '$id'" >&2; exit 13 ;;
|
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
|
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
|
// Proxmox guest power via a SCOPED PVEAPIToken (VM.PowerMgmt on whitelisted guests
|
||||||
// only). PVE enforces permissions server-side; this adapter never builds shell commands.
|
// only). PVE enforces permissions server-side; this adapter never builds shell commands.
|
||||||
export async function powerGuest({ node, vmid, op, kindPath = 'lxc' }, {
|
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 url = `${apiUrl}/api2/json/nodes/${node}/${kindPath}/${vmid}/status/${op}`;
|
||||||
const res = await fetchImpl(url, {
|
const res = await fetchImpl(url, {
|
||||||
method: 'POST',
|
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?.() ?? ''}`);
|
if (!res.ok) throw new Error(`proxmox ${op} ${vmid} → ${res.status} ${await res.text?.() ?? ''}`);
|
||||||
const body = await res.json();
|
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.
|
// its OWN whitelist. We pass ONLY the id as a single argv element — no shell.
|
||||||
export function restartService({ ip, actionId }, {
|
export function restartService({ ip, actionId }, {
|
||||||
keyPath = process.env.ACTIONS_SSH_KEY,
|
keyPath = process.env.ACTIONS_SSH_KEY,
|
||||||
user = 'voidact',
|
user = process.env.ACTIONS_SSH_USER || 'voidact',
|
||||||
spawnImpl = nodeSpawn
|
spawnImpl = nodeSpawn
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!ID_RE.test(actionId || '')) return Promise.reject(new Error(`invalid action id: ${actionId}`));
|
if (!ID_RE.test(actionId || '')) return Promise.reject(new Error(`invalid action id: ${actionId}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user