#!/bin/bash # VM entrypoint, exec'd as PID 1 by /boco/init (image runs as root). # Starts sshd for ops access, prepares virtiofs share ownership, then # drops to the marvin user and execs turnstone-server. set -euo pipefail export HOME=/home/marvin # Set hostname here since podman prevents us from setting it during # build.-time. Turnstone requires a hostname to be set. python3 -c 'import socket; socket.sethostname("marvin-vm")' || true # sshd: host keys baked per-boot, ops channel on guest port 22 ssh-keygen -A mkdir -p /run/sshd /usr/sbin/sshd # virtiofs shares appear root-owned (host dir is owned by the host user, # which maps to guest root) — hand them to marvin so the server and # agent tools can write. Also ensure ~/.config stays marvin-owned: # boco-init creates mount-point dirs as root, and rootless podman # refuses to run when $HOME/.config is not owned by the user. mkdir -p /var/lib/turnstone /home/marvin/.config chown marvin:marvin /var/lib/turnstone /home/marvin/.config || true chown -R marvin:marvin /home/marvin/work || true # Rootless container networking (pasta/slirp4netns) needs TUN access; # the guest has no udev, so devtmpfs leaves it root-only. if [ ! -e /dev/net/tun ]; then mkdir -p /dev/net [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200 fi chmod 666 /dev/net/tun || true # Seed the admin account (no-op when the user already exists — password # is ignored on promotion). Runs as marvin so DB files it creates are # marvin-owned. Override via image env or boot cmd: # boco boot ... --cmd 'ADMIN_PASSWORD=... start-turnstone.sh' ADMIN_USER="${ADMIN_USER:-marvin}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-marvin-turnstone}" if [ "$ADMIN_PASSWORD" = "marvin-turnstone" ]; then echo "start-turnstone: WARNING — default admin password in use; set ADMIN_PASSWORD to change it" fi setpriv --reuid=marvin --regid=marvin --init-groups \ /usr/local/bin/turnstone-admin create-admin \ --username "$ADMIN_USER" \ --password "$ADMIN_PASSWORD" \ || echo "start-turnstone: admin seeding failed (continuing)" # Console (cluster dashboard + admin panel) as a background daemon under # marvin — PID 1 stays the server, so if the server dies the VM halts. # Shares the same config.toml/database as the server (WAL multi-writer). setpriv --reuid=marvin --regid=marvin --init-groups \ /usr/local/bin/turnstone-console --host 0.0.0.0 --port 8080 & echo "start-turnstone: console starting on :8080" # NOTE: turnstone reads $HOME/.config/turnstone/config.toml as marvin. # # Stable node identity: node_id defaults to {hostname}_{random-per-boot}, # so every boot inserts a NEW services row and the console probes stale # ones (409 node_id mismatch → "unreachable"). Pin both: # - TURNSTONE_NODE_ID: one registry row, upserted every boot # - TURNSTONE_ADVERTISE_URL: loopback — the console's collector probes # the registered URL, and the guest's slirp IPv6 (fec0::/fe80::) # lands in the SSRF guard's never-allowed lane, which got the node # refused outright. export TURNSTONE_NODE_ID="${TURNSTONE_NODE_ID:-turnstone-vm-main}" export TURNSTONE_ADVERTISE_URL="${TURNSTONE_ADVERTISE_URL:-http://127.0.0.1:8081}" exec setpriv --reuid=marvin --regid=marvin --init-groups \ /usr/local/bin/turnstone-server --host 0.0.0.0 --port 8081