Addresses the two valid items from the independent review (code/security/infra all
Approved; these were Minor/Needs-Change items).
1. provision.sh left the nginx vhost as a manual "see README" step, which broke the
project's own repeatability requirement (FR-9): a rebuild from scratch would have
served the distro default page instead of Atheos. The vhost is now written by the
script itself, the distro default.conf is removed, and the script ASSERTS the socket
contract (nginx fastcgi_pass == php-fpm pool listen) before Atheos is installed - so
a 502 can only ever mean an nginx/php-fpm problem, never be mistaken for an Atheos one.
2. `free` and `uptime` reported the PVE HOST inside the container (a container that had
restarted seconds earlier claimed "up 2 days"), which is misleading in monitoring and
in the test output.
The review suggested `apk add lxcfs` in the guest. That is NOT the fix - lxcfs is a
host-provided FUSE filesystem, not a guest package, and measurement showed it is
already working correctly here:
/proc/meminfo -> MemTotal: 1048576 kB (our 1024 MB limit)
/proc/uptime -> container seconds
/proc/cpuinfo -> 2 processors
The real cause: on Alpine, `free`/`uptime` are BUSYBOX applets that read via the
sysinfo() SYSCALL, which lxcfs cannot intercept. procps-ng ships binaries that read
the lxcfs-virtualised /proc files instead. Measured on the guest:
procps-ng free -> 1024 MB (correct)
busybox free -> 15701 MB (host)
procps-ng uptime -> up 2 min (correct)
busybox uptime -> up 2 days (host)
So provision.sh now installs procps-ng, and build-and-test.sh reports container-
accurate figures (detecting it via apk rather than a binary-existence probe, since
the busybox applets are plain files too).
Verified after the change: build-and-test 40 passed / 0 failed; socket contract MATCH;
resource line now reads "18MB used of 1024MB, up 2 min".
247 lines
11 KiB
Bash
247 lines
11 KiB
Bash
#!/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://<guest-ip>/ 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."
|