From d98a63c0f1bc7aaa8b3f12ef5c0e199e2bc52e12 Mon Sep 17 00:00:00 2001 From: byrsapty Date: Wed, 22 Jul 2026 22:17:02 +0300 Subject: [PATCH] Automate the AppArmor fix with a narrowly-scoped sudoers rule Adds bootstrap-host.sh (one-time, run as root on a fresh Proxmox host) and fix-lxc-apparmor.sh, the fixed-content script it installs. The sudoers rule it wires up only ever invokes that one root-owned script with a VMID argument - deliberately not a broader rule like `tee -a ` or `sh -c '...'`, since those only restrict the command's own argv, not stdin/heredoc content, letting the caller write arbitrary lines to any 200-299 container's config instead of just this one fixed line. create-graylog-lxc.sh now tries `sudo -n fix-lxc-apparmor.sh` first and falls back to the existing manual instructions if that sudoers rule isn't present yet - fully backward compatible with hosts that haven't run bootstrap-host.sh. Co-Authored-By: Claude Sonnet 5 --- README.md | 26 +++++++++++++++++++++++-- README.uk.md | 27 +++++++++++++++++++++++--- bootstrap-host.sh | 44 +++++++++++++++++++++++++++++++++++++++++++ create-graylog-lxc.sh | 12 +++++++++--- fix-lxc-apparmor.sh | 24 +++++++++++++++++++++++ 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 bootstrap-host.sh create mode 100644 fix-lxc-apparmor.sh diff --git a/README.md b/README.md index 3afaedc..8c040ee 100644 --- a/README.md +++ b/README.md @@ -290,14 +290,36 @@ alert templates already do it. - **Docker-in-unprivileged-LXC AppArmor block**: containers fail with `open sysctl net.ipv4.ip_unprivileged_port_start: permission denied` unless the Proxmox host admin adds a raw LXC config line. `pct set` - does not expose this option, so it cannot be automated within the - `claude-deploy` sudo scope. If you hit this, run as root on the host: + does not expose this option (checked: `--features` covers + `nesting`/`keyctl`/`mount`/`fuse`/`mknod`/`force_rw_sys`, nothing for + AppArmor), so it can't be automated within the plain `claude-deploy` + sudo scope of `pct create/set/start/stop/exec/status/list`. If you hit + this, run as root on the host: ```bash echo "lxc.apparmor.profile: unconfined" >> /etc/pve/lxc/.conf pct reboot ``` then re-run `create-graylog-lxc.sh` (idempotent, will continue from there). + **Automating this fix (optional, one-time per Proxmox host):** run + `bootstrap-host.sh` once as root. It installs `fix-lxc-apparmor.sh` to + `/usr/local/sbin/` (root-owned, `chmod 700` - not writable by + `claude-deploy`) plus a narrow sudoers rule scoped to exactly that script + and the VMID 200-299 range: + ``` + claude-deploy ALL=(root) NOPASSWD: /usr/local/sbin/fix-lxc-apparmor.sh 2[0-9][0-9] + ``` + Deliberately *not* a broader rule like `tee -a /etc/pve/lxc/2[0-9][0-9].conf` + or `sh -c '...'`: sudoers only restricts a command's own argv, not + stdin/heredoc content, so either of those would let the caller append + *arbitrary* lines to any 200-299 container's config (e.g. + `lxc.mount.entry` to bind-mount host paths in) - a much wider grant than + intended. Pinning the exact line inside a fixed, root-owned script is + what keeps the sudoers rule's effect as narrow as its pattern suggests. + Once bootstrapped, `create-graylog-lxc.sh` calls + `sudo -n fix-lxc-apparmor.sh` automatically and only falls back to the + manual instructions above if that sudoers rule isn't present yet. + - **`vm.max_map_count`**: OpenSearch requires >= 262144. This is a host-wide kernel parameter, not namespaced per LXC, so it can't be set from inside the container either. `install-graylog.sh` only verifies it diff --git a/README.uk.md b/README.uk.md index 5c631fd..a8276aa 100644 --- a/README.uk.md +++ b/README.uk.md @@ -293,9 +293,11 @@ Graylog відхиляє його з помилкою Jackson-поліморфі - **Блокування Docker-в-unprivileged-LXC через AppArmor**: контейнери падають з помилкою `open sysctl net.ipv4.ip_unprivileged_port_start: permission denied`, якщо адміністратор хоста не додасть сирий рядок - конфігурації LXC. `pct set` не підтримує цю опцію, тож автоматизувати - це в межах прав `claude-deploy` неможливо. Якщо зіткнетесь із цим, - виконайте під root на хості: + конфігурації LXC. `pct set` не підтримує цю опцію (перевірено: + `--features` покриває `nesting`/`keyctl`/`mount`/`fuse`/`mknod`/ + `force_rw_sys`, нічого для AppArmor), тож автоматизувати це в межах + звичайних прав `claude-deploy` (`pct create/set/start/stop/exec/status/list`) + неможливо. Якщо зіткнетесь із цим, виконайте під root на хості: ```bash echo "lxc.apparmor.profile: unconfined" >> /etc/pve/lxc/.conf pct reboot @@ -303,6 +305,25 @@ Graylog відхиляє його з помилкою Jackson-поліморфі а потім перезапустіть `create-graylog-lxc.sh` (ідемпотентний, продовжить з цього місця). + **Автоматизація цього фіксу (опційно, одноразово на кожен Proxmox-хост):** + запустіть `bootstrap-host.sh` один раз від root. Він встановлює + `fix-lxc-apparmor.sh` у `/usr/local/sbin/` (власник root, `chmod 700` — + недоступний для запису `claude-deploy`) плюс вузьке sudoers-правило, + прив'язане саме до цього скрипта й діапазону VMID 200-299: + ``` + claude-deploy ALL=(root) NOPASSWD: /usr/local/sbin/fix-lxc-apparmor.sh 2[0-9][0-9] + ``` + Навмисно *не* ширше правило типу `tee -a /etc/pve/lxc/2[0-9][0-9].conf` + чи `sh -c '...'`: sudoers обмежує лише власні аргументи команди, а не + вміст stdin/heredoc, тож будь-який з цих варіантів дозволив би дописати + *довільні* рядки в конфіг будь-якого контейнера 200-299 (наприклад, + `lxc.mount.entry` для монтування хостових шляхів усередину) — набагато + ширший доступ, ніж потрібно. Саме фіксація точного рядка всередині + незмінного, root-owned скрипта тримає ефект sudoers-правила таким же + вузьким, як і виглядає його патерн. Після bootstrap `create-graylog-lxc.sh` + сам викликає `sudo -n fix-lxc-apparmor.sh` і повертається до ручної + інструкції вище лише якщо цього sudoers-правила ще немає. + - **`vm.max_map_count`**: OpenSearch вимагає >= 262144. Це загальносистемний параметр ядра хоста, не прив'язаний до конкретного LXC, тож встановити його зсередини контейнера теж неможливо. diff --git a/bootstrap-host.sh b/bootstrap-host.sh new file mode 100644 index 0000000..33b4cc3 --- /dev/null +++ b/bootstrap-host.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Run ONCE, as root, on a fresh Proxmox host - not part of the claude-deploy +# workflow, since claude-deploy is exactly the account this script is +# widening permissions FOR. Idempotent: safe to re-run. +# +# Installs fix-lxc-apparmor.sh (root-owned, not writable by claude-deploy) +# and a narrow sudoers rule that lets claude-deploy invoke it - and only +# it, only with a VMID argument in the 200-299 range - so create-graylog-lxc.sh +# can clear the "Docker-in-unprivileged-LXC AppArmor block" quirk (see +# README.md) without a human editing /etc/pve/lxc/*.conf by hand every time. +# +# Deliberately does NOT grant broader access (e.g. `tee -a` or `sh -c` on +# the conf file): those let the caller write arbitrary content to any +# 200-299 container's config, not just this one fixed line. See the +# "Automating the AppArmor fix" note in README.md for why. +set -euo pipefail + +[ "$(id -u)" -eq 0 ] || { echo "Must run as root." >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET="/usr/local/sbin/fix-lxc-apparmor.sh" +SUDOERS_FILE="/etc/sudoers.d/claude-deploy-apparmor" +SUDOERS_LINE="claude-deploy ALL=(root) NOPASSWD: ${TARGET} 2[0-9][0-9]" + +if [ -f "$TARGET" ] && cmp -s "$SCRIPT_DIR/fix-lxc-apparmor.sh" "$TARGET"; then + echo "⏭ $TARGET already up to date" +else + install -o root -g root -m 700 "$SCRIPT_DIR/fix-lxc-apparmor.sh" "$TARGET" + echo "✓ installed $TARGET (root-owned, 700)" +fi + +if [ -f "$SUDOERS_FILE" ] && grep -qxF "$SUDOERS_LINE" "$SUDOERS_FILE"; then + echo "⏭ sudoers rule already present in $SUDOERS_FILE" +else + tmp="$(mktemp)" + echo "$SUDOERS_LINE" > "$tmp" + chmod 440 "$tmp" + visudo -c -f "$tmp" || { echo "generated sudoers snippet failed validation" >&2; rm -f "$tmp"; exit 1; } + mv "$tmp" "$SUDOERS_FILE" + chown root:root "$SUDOERS_FILE" + echo "✓ installed sudoers rule at $SUDOERS_FILE" +fi + +echo "Done. claude-deploy can now run: sudo $TARGET " diff --git a/create-graylog-lxc.sh b/create-graylog-lxc.sh index c95e7a8..06825cc 100644 --- a/create-graylog-lxc.sh +++ b/create-graylog-lxc.sh @@ -181,12 +181,18 @@ start_container_and_wait_net() { } check_apparmor_hint() { - # We cannot fix this ourselves (no permission to edit /etc/pve/lxc/*.conf), - # so just remind the operator up front; install-graylog.sh will hard-stop - # with the same instructions if it's actually needed. + # If bootstrap-host.sh has been run on this host, the narrow sudoers rule + # for fix-lxc-apparmor.sh exists and we can just fix this ourselves. + # Otherwise fall back to the same manual instructions as before - this + # must never hard-fail the script over an optional convenience path. + if sudo -n /usr/local/sbin/fix-lxc-apparmor.sh "$VMID" 2>/dev/null; then + ok "AppArmor profile fixed automatically (bootstrap-host.sh was run on this host)" + return + fi log "NOTE: if Docker fails inside the container with a sysctl/AppArmor permission error," log " the host admin must run:" log " echo 'lxc.apparmor.profile: unconfined' >> /etc/pve/lxc/${VMID}.conf && pct reboot ${VMID}" + log " (or run bootstrap-host.sh once as root to automate this for good)" } copy_install_payload() { diff --git a/fix-lxc-apparmor.sh b/fix-lxc-apparmor.sh new file mode 100644 index 0000000..ad25999 --- /dev/null +++ b/fix-lxc-apparmor.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Installed by bootstrap-host.sh to /usr/local/sbin/fix-lxc-apparmor.sh, +# owned by root, NOT writable by claude-deploy - only invocable via the +# narrow sudoers rule bootstrap-host.sh also installs. +# +# Idempotently enables lxc.apparmor.profile=unconfined for one LXC container +# (needed for Docker to run inside an unprivileged container - see the +# "AppArmor" quirk in README.md) and reboots it if a change was actually +# made. The VMID range is enforced by the sudoers rule that invokes this +# script, not by this script itself. +set -euo pipefail +VMID="${1:?Usage: fix-lxc-apparmor.sh }" +CONF="/etc/pve/lxc/${VMID}.conf" + +[ -f "$CONF" ] || { echo "No such container config: $CONF" >&2; exit 1; } + +if grep -qx "lxc.apparmor.profile: unconfined" "$CONF"; then + echo "already set, nothing to do" + exit 0 +fi + +echo "lxc.apparmor.profile: unconfined" >> "$CONF" +pct reboot "$VMID" +echo "applied and rebooted $VMID"