mcp: restricted-shell SSH proxy through hubris for management tools
The 5 management tools (get_service_status, tail_log, list_lxcs, get_lxc_state, ping_service) were registered with sse but all SSH calls went to per-host targets via a 'mcp-reader' user that didn't exist anywhere. New design routes every management call through ONE channel: LXC 105 -> hubris (SSH key + restricted authorized_keys command), then hubris pct-execs into the right LXC where needed. Adds mcp/mcp-reader-shell — a strict allowlist wrapper read from $SSH_ORIGINAL_COMMAND. Rejects shell metacharacters up front and then matches against a fixed set of read-only patterns (systemctl is-active/ is-enabled, journalctl -u, pct list/status/config, pct exec for the same subset). Logged to syslog tag mcp-reader. Authorized_keys line on hubris: command="/usr/local/bin/mcp-reader-shell",restrict ssh-ed25519 ... mcp-reader@homelab-mcp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
62
mcp/mcp-reader-shell
Executable file
62
mcp/mcp-reader-shell
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# mcp-reader-shell — restricted SSH command for the homelab-mcp service.
|
||||
#
|
||||
# Authorized in /root/.ssh/authorized_keys on hubris via:
|
||||
# command="/usr/local/bin/mcp-reader-shell",restrict ssh-ed25519 AAAA... mcp-reader@homelab-mcp
|
||||
#
|
||||
# `restrict` disables PTY/agent/forwarding/X11. This wrapper then validates
|
||||
# $SSH_ORIGINAL_COMMAND against a strict read-only allowlist before running
|
||||
# it. Anything outside the allowlist (interactive shell, file writes, pct
|
||||
# start/stop/destroy, etc.) is refused.
|
||||
#
|
||||
# Distributed via the homelab-context sync — symlink:
|
||||
# /usr/local/bin/mcp-reader-shell -> /opt/homelab-context/mcp/mcp-reader-shell
|
||||
# so updates land on the next 5-min pull without a manual re-install.
|
||||
#
|
||||
# Argument shapes allowed (Bash glob, after rejecting shell metacharacters):
|
||||
# systemctl is-active <unit>
|
||||
# systemctl is-enabled <unit>
|
||||
# journalctl -u <unit> [-n N] [--no-pager]
|
||||
# pct list
|
||||
# pct status <id>
|
||||
# pct config <id>
|
||||
# pct exec <id> -- systemctl is-active <unit>
|
||||
# pct exec <id> -- systemctl is-enabled <unit>
|
||||
# pct exec <id> -- journalctl -u <unit> [-n N] [--no-pager]
|
||||
#
|
||||
# Logs each call to syslog via `logger`. Deny entries are warnings.
|
||||
|
||||
set -euo pipefail
|
||||
set -f # disable glob expansion when we exec the command
|
||||
|
||||
CMD="${SSH_ORIGINAL_COMMAND:-}"
|
||||
|
||||
deny() {
|
||||
logger -t mcp-reader -p auth.warning "DENY from=${SSH_CLIENT:-?}: ${CMD:-<empty>}"
|
||||
echo "mcp-reader: command not allowed" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "$CMD" ]; then
|
||||
deny
|
||||
fi
|
||||
|
||||
# Reject any shell metacharacter that would let an attacker chain or escape
|
||||
# from the patterns below.
|
||||
if [[ "$CMD" =~ [\;\&\|\>\<\`\$\\\(\)\{\}\*\?\~\!] ]]; then
|
||||
deny
|
||||
fi
|
||||
|
||||
case "$CMD" in
|
||||
"systemctl is-active "*|"systemctl is-enabled "*) ;;
|
||||
"journalctl -u "*) ;;
|
||||
"pct list") ;;
|
||||
"pct status "*|"pct config "*) ;;
|
||||
"pct exec "*" -- systemctl is-active "*) ;;
|
||||
"pct exec "*" -- systemctl is-enabled "*) ;;
|
||||
"pct exec "*" -- journalctl -u "*) ;;
|
||||
*) deny ;;
|
||||
esac
|
||||
|
||||
logger -t mcp-reader -p auth.info "ALLOW from=${SSH_CLIENT:-?}: $CMD"
|
||||
exec $CMD
|
||||
Reference in New Issue
Block a user