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 <conf>` 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 <noreply@anthropic.com>
This commit is contained in:
byrsapty 2026-07-22 22:17:02 +03:00
parent 458aada5a5
commit d98a63c0f1
5 changed files with 125 additions and 8 deletions

View file

@ -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/<VMID>.conf
pct reboot <VMID>
```
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

View file

@ -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/<VMID>.conf
pct reboot <VMID>
@ -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, тож встановити його зсередини контейнера теж неможливо.

44
bootstrap-host.sh Normal file
View file

@ -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 <vmid>"

View file

@ -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() {

24
fix-lxc-apparmor.sh Normal file
View file

@ -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 <vmid>}"
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"