#!/bin/sh # ============================================================================== # provision.sh — rebuild the Atheos blog editor LXC from scratch # Project: atheos-blog-lxc (AI-DLC) # Target : Proxmox LXC 136 on node scotty (192.168.178.69) # # RUN THIS ON THE PVE NODE (scotty), as root. It is written to be re-runnable: # each step checks state before acting. # # This script reproduces exactly what was executed during the initial build on # 2026-09-29. It deliberately does NOT create any `armistace` credential, git # identity, or Gitea user (requirement FR-11). # ============================================================================== set -eu VMID=136 NODE_HOST="192.168.178.69" TEMPLATE_STORAGE="LARGE_STORAGE_1" ROOTFS_STORAGE="scotty-local-lvm" TEMPLATE="alpine-3.23-default_20260116_amd64.tar.xz" BRIDGE="vmbr0" ROOTFS_SIZE=20 CORES=2 MEMORY=1024 SWAP=256 TZ_NAME="Australia/Brisbane" # --- Atheos pin (design decision D2: pinned, not tracking a moving branch) ---- # Recorded at build time 2026-09-29 from github.com/Atheos/Atheos (default branch `main`) ATHEOS_SHA="f90cd1806dbe0e25673a77e27390e8b1a240c3f6" ATHEOS_BUILD=611 ATHEOS_TARBALL_SHA256="03ebcb1479b59f1898b5fa50808b181750b42211698f7f7b4042cf2514d78b20" # --- the blog repo (read-only clone source; agent never pushes) -------------- BLOG_REPO="https://git.aridgwayweb.com/armistace/blog.git" GITEA_SSH_HOST="192.168.178.155" # LAN address: Gitea SSH_DOMAIN; public :22 is not forwarded GITEA_SSH_USER="gitea" # !! app.ini SSH_USER=gitea - NOT the usual `git`. # Using `git` yields "Invalid user git" from sshd, # which is rejected BEFORE authorized_keys is read. WEBROOT="/var/www/atheos" FPM_USER="atheos" FPM_SOCKET="/run/php-fpm83/php-fpm.sock" log() { printf '\n=== %s\n' "$*"; } # ============================================================================== log "1/9 verify we are on the PVE node and the VMID is free" hostname pct status "$VMID" >/dev/null 2>&1 && { echo "ERROR: VMID $VMID already exists - aborting"; exit 1; } # ============================================================================== log "2/9 ensure the Alpine template is present" pveam update >/dev/null 2>&1 || true if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" else echo "template already present" fi # ============================================================================== log "3/9 create the container (U1)" pct create "$VMID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" \ --hostname atheos-blog \ --cores "$CORES" \ --memory "$MEMORY" \ --swap "$SWAP" \ --rootfs "$ROOTFS_STORAGE:$ROOTFS_SIZE" \ --net0 "name=eth0,bridge=$BRIDGE,ip=dhcp" \ --unprivileged 1 \ --onboot 1 \ --ostype alpine \ --features "" \ --description "Atheos browser editor for armistace/blog (AI-DLC project atheos-blog-lxc). LAN-only. Deploy key auth. See Gitea hermes/atheos-blog-lxc RUNBOOK." pct start "$VMID" sleep 8 pct status "$VMID" # ============================================================================== log "4/9 base configuration (U1): timezone, ssh, apt/apk index" pct exec "$VMID" -- sh -c " apk update >/dev/null 2>&1 apk add --no-cache tzdata openssh >/dev/null 2>&1 cp /usr/share/zoneinfo/$TZ_NAME /etc/localtime echo $TZ_NAME > /etc/timezone rc-update add sshd default >/dev/null 2>&1 rc-service sshd start >/dev/null 2>&1 date " # ============================================================================== log "5/9 web stack (U2): nginx + php83 + extensions" pct exec "$VMID" -- sh -c ' apk add --no-cache nginx php83 php83-fpm \ php83-session php83-zip php83-openssl php83-mbstring php83-ctype php83-curl \ php83-dom php83-json php83-iconv php83-fileinfo php83-simplexml php83-xml \ php83-tokenizer procps-ng >/dev/null 2>&1 # procps-ng (NOT lxcfs) is what makes free/uptime report the CONTAINER rather than the host. # busybox applets use the sysinfo() syscall, which lxcfs cannot intercept; procps-ng reads the # lxcfs-virtualised /proc/meminfo and /proc/uptime. Verified: procps free -> 1024MB, busybox -> host. # dedicated service user (I-4: FPM user == file owner == key owner) adduser -D -s /sbin/nologin -h /home/atheos atheos 2>/dev/null || true mkdir -p /var/www/atheos/workspace /run/php-fpm83 /var/log/nginx chown nginx:nginx /run/php-fpm83 chmod 755 /run/php-fpm83 chown -R atheos:atheos /var/www/atheos /home/atheos # timezone (NFR-5) sed -i "s|^;\?date.timezone =.*|date.timezone = Australia/Brisbane|" /etc/php83/php.ini # --- the socket contract (I-1): pool listen MUST byte-match nginx fastcgi_pass --- cp -n /etc/php83/php-fpm.d/www.conf /etc/php83/php-fpm.d/www.conf.orig 2>/dev/null || true sed -i "s|^user = .*|user = atheos|" /etc/php83/php-fpm.d/www.conf sed -i "s|^group = .*|group = atheos|" /etc/php83/php-fpm.d/www.conf sed -i "s|^listen = .*|listen = '"$FPM_SOCKET"'|" /etc/php83/php-fpm.d/www.conf grep -q "^listen.owner" /etc/php83/php-fpm.d/www.conf || echo "listen.owner = nginx" >> /etc/php83/php-fpm.d/www.conf grep -q "^listen.group" /etc/php83/php-fpm.d/www.conf || echo "listen.group = nginx" >> /etc/php83/php-fpm.d/www.conf grep -q "^listen.mode" /etc/php83/php-fpm.d/www.conf || echo "listen.mode = 0660" >> /etc/php83/php-fpm.d/www.conf rc-update add nginx default >/dev/null 2>&1 rc-update add php-fpm83 default >/dev/null 2>&1 ' # --- nginx vhost: installed BY THE SCRIPT so provisioning is repeatable end-to-end ----------- # (review finding: leaving this as a manual "see README" step broke the project's own # repeatability requirement FR-9 — a rebuild from scratch would serve the distro default page) pct exec "$VMID" -- sh -c "cat > /etc/nginx/http.d/atheos.conf" <<'NGINXCONF' server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /var/www/atheos; index index.php index.html; access_log /var/log/nginx/atheos.access.log; error_log /var/log/nginx/atheos.error.log; # The socket path here MUST byte-match the pool's `listen =` in # /etc/php83/php-fpm.d/www.conf (interface I-1 - a mismatch yields a bare 502). location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/run/php-fpm83/php-fpm.sock; fastcgi_index index.php; include fastcgi.conf; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location / { try_files $uri $uri/ =404; } # never serve the VCS dir, the app's private data, or dotfiles location ~ /\.git { deny all; } location ~ /\.ssh { deny all; } location ~ /\. { deny all; } location ~ ^/data/ { deny all; } location ~ ^/(config\.php|common\.php|controller\.php|traits/) { deny all; } location ~* \.(md|py|sh|log|conf|ini|sql|bak|orig)$ { deny all; } add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; client_max_body_size 64m; } NGINXCONF # remove the distro default vhost so ours is authoritative, then start the stack pct exec "$VMID" -- sh -c ' rm -f /etc/nginx/http.d/default.conf rc-service php-fpm83 restart >/dev/null 2>&1 rc-service nginx restart >/dev/null 2>&1 sleep 2 # prove the socket contract holds BEFORE Atheos is installed, so a 502 can only ever # mean an nginx/php-fpm problem and never be confused with an Atheos problem SOCK=$(sed -n "s/^listen = //p" /etc/php83/php-fpm.d/www.conf | head -1) NGX=$(sed -n "s/.*fastcgi_pass unix:\([^;]*\);.*/\1/p" /etc/nginx/http.d/atheos.conf | head -1) if [ "$SOCK" = "$NGX" ]; then echo "socket contract OK: $SOCK"; else echo "SOCKET MISMATCH: pool=$SOCK nginx=$NGX"; exit 1; fi ' # ============================================================================== log "6/9 Atheos install at the pinned commit (U3)" pct exec "$VMID" -- sh -c " cd /tmp wget -q -O atheos.tar.gz https://codeload.github.com/Atheos/Atheos/tar.gz/$ATHEOS_SHA echo '$ATHEOS_TARBALL_SHA256 atheos.tar.gz' | sha256sum -c - || { echo 'CHECKSUM MISMATCH - abort'; exit 1; } rm -rf /tmp/atheos-extract && mkdir -p /tmp/atheos-extract tar xzf atheos.tar.gz -C /tmp/atheos-extract cd /tmp/atheos-extract/Atheos-$ATHEOS_SHA for f in \$(ls -A); do [ \"\$f\" = workspace ] && continue cp -a \"\$f\" $WEBROOT/ 2>/dev/null || true done cd $WEBROOT touch config.php && chmod 664 config.php for d in data workspace plugins themes; do mkdir -p \$d; chmod 775 \$d; done chown -R atheos:atheos $WEBROOT echo \"Atheos build: \$(cat .version)\" " # ============================================================================== log "7/9 git + clone + deploy key (U4)" pct exec "$VMID" -- sh -c " apk add --no-cache git openssh-client >/dev/null 2>&1 cd $WEBROOT/workspace su -s /bin/sh atheos -c 'git config --global --add safe.directory $WEBROOT/workspace' su -s /bin/sh atheos -c 'git config --global --add safe.directory $WEBROOT/workspace/blog' # clone over HTTPS (public repo => no credential at this stage; U4 phase 1) su -s /bin/sh atheos -c 'test -d blog/.git || git clone $BLOG_REPO blog' # deploy keypair: private key stays on the guest, owned by the FPM user mkdir -p /home/atheos/.ssh && chmod 700 /home/atheos/.ssh && chown atheos:atheos /home/atheos/.ssh su -s /bin/sh atheos -c 'test -f /home/atheos/.ssh/id_ed25519_blog || ssh-keygen -t ed25519 -f /home/atheos/.ssh/id_ed25519_blog -N \"\" -C \"atheos-blog-lxc deploy key (armistace/blog)\"' chmod 600 /home/atheos/.ssh/id_ed25519_blog chmod 644 /home/atheos/.ssh/id_ed25519_blog.pub chown atheos:atheos /home/atheos/.ssh/id_ed25519_blog /home/atheos/.ssh/id_ed25519_blog.pub " # NB the ~/.ssh/config alias is written by the install-files step (see files/ssh-config) # IMPORTANT: its User must be "$GITEA_SSH_USER" (gitea), NOT `git`. pct exec "$VMID" -- sh -c "chmod 600 /home/atheos/.ssh/config 2>/dev/null; chown atheos:atheos /home/atheos/.ssh/config 2>/dev/null; true" # ============================================================================== log "8/9 PUBLISH the deploy key public half to the operator" pct exec "$VMID" -- sh -c " echo echo ' ----- BEGIN deploy key (public half) -----' cat /home/atheos/.ssh/id_ed25519_blog.pub echo ' ----- END deploy key (public half) -----' ssh-keygen -lf /home/atheos/.ssh/id_ed25519_blog.pub " cat <<'INSTRUCTIONS' >>> OPERATOR ACTION: add the key above to Gitea Repo: armistace/blog -> Settings -> Deploy Keys -> Add Deploy Key Title: atheos-blog-lxc TICK "Allow write access" (required for push) Then tell the agent to run phase 2 (switch the remote to SSH): git -C /var/www/atheos/workspace/blog remote set-url origin gitea-blog:armistace/blog.git INSTRUCTIONS # ============================================================================== log "9/9 smoke test" pct exec "$VMID" -- sh -c ' ip -4 addr show eth0 | grep inet rc-service nginx status | head -1 rc-service php-fpm83 status | head -1 ls -lad /run/php-fpm83/php-fpm.sock ' echo echo "Provisioning complete. Now open http:/// in a browser and complete" echo "the Atheos first-run installer (creates the admin user - operator owned)." echo "IMPORTANT: first action after SSH -> set your git identity. See RUNBOOK.md step 1."