diff --git a/deploy/zopu-runtime/README.md b/deploy/zopu-runtime/README.md index 99e1c67..28f94e9 100644 --- a/deploy/zopu-runtime/README.md +++ b/deploy/zopu-runtime/README.md @@ -33,7 +33,7 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure. │ │ │ systemd timers: health-check (60s), docker-cleanup (daily) │ │ cron: disk-monitor (daily 06:00) │ -│ ufw: deny-by-default, SSH only │ +│ ufw: deny-incoming, SSH + tailscale0 │ │ Tailscale: optional private overlay │ └─────────────────────────────────────────────────────────────┘ ``` @@ -265,16 +265,25 @@ Default alert threshold is 80%. ## Firewall and private networking The firewall (`ufw`) is deny-by-default: -- SSH (port 22) is allowed +- SSH (port 22) is allowed on all interfaces +- All traffic on `tailscale0` is allowed (Tailscale private overlay) - All other incoming traffic is denied The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** -exposed publicly. Access is via: +exposed on public interfaces. Reachability options: -1. **Tailscale** (recommended): private overlay network. Set +1. **Tailscale** (recommended): bootstrap runs `ufw allow in on tailscale0` + so all ports are reachable over the private overlay. Set `TAILSCALE_AUTHKEY` before running bootstrap to configure automatically. -2. **Direct private IP**: if the server is on a private network, the ports - are reachable from other machines on the same subnet. + Other Tailscale-connected machines can reach the agent at + `http://zopu-runtime:3583` and the engine at `http://zopu-runtime:6420`. +2. **Custom private interface**: if you have a non-Tailscale private network + (e.g. a VLAN or wireguard interface), add an explicit rule: + ```bash + ufw allow in on eth1 # or your private interface name + ``` + Do NOT assume direct private IP access works by default — the deny-incoming + policy blocks it until an interface-specific rule is added. 3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in `caddy/` if you need TLS termination or a public ingress point. diff --git a/deploy/zopu-runtime/bootstrap.sh b/deploy/zopu-runtime/bootstrap.sh index 1f42897..68e5deb 100755 --- a/deploy/zopu-runtime/bootstrap.sh +++ b/deploy/zopu-runtime/bootstrap.sh @@ -4,7 +4,7 @@ # # Run as root on a fresh Debian 12 host: # -# sudo bash bootstrap.sh +# bash bootstrap.sh # # Environment overrides (set before running): # ZOPU_REPO_URL — SSH clone URL (default: ssh://git@git.openputer.com:2222/puter/zopu-code.git) @@ -38,6 +38,12 @@ log() { echo -e "${GREEN}[bootstrap]${NC} $*"; } warn() { echo -e "${YELLOW}[bootstrap]${NC} $*"; } err() { echo -e "${RED}[bootstrap]${NC} $*" >&2; } +# runuser is part of util-linux (essential on Debian) and always available. +# sudo is NOT assumed on minimal Debian installs. +run_as_service() { + runuser -u "$SERVICE_USER" -- "$@" +} + # --------------------------------------------------------------------------- # Pre-flight # --------------------------------------------------------------------------- @@ -64,9 +70,9 @@ apt-get install -y \ ufw \ git \ jq \ + netcat-openbsd \ openssh-client \ unattended-upgrades \ - netcat-openbsd \ rsyslog # --------------------------------------------------------------------------- @@ -137,17 +143,23 @@ else cd "$INSTALL_DIR" fi +# --------------------------------------------------------------------------- +# 5b. Hand ownership of the checkout to the service user +# --------------------------------------------------------------------------- +log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..." +chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR" + # --------------------------------------------------------------------------- # 6. Install dependencies and build # --------------------------------------------------------------------------- log "Running bun install..." -sudo -u "$SERVICE_USER" bun install +run_as_service bun install log "Building daemon binary..." -sudo -u "$SERVICE_USER" bun run build:daemon +run_as_service bun run build:daemon log "Building agent service..." -sudo -u "$SERVICE_USER" bun run build:agents +run_as_service bun run build:agents # --------------------------------------------------------------------------- # 7. Environment file @@ -161,6 +173,9 @@ if [[ ! -f "$ENV_FILE" ]]; then warn "Edit $ENV_FILE with real values before starting services." else log ".env already exists at $ENV_FILE" + # Ensure correct ownership and permissions on existing .env + chown "$SERVICE_USER":"$SERVICE_USER" "$ENV_FILE" + chmod 600 "$ENV_FILE" fi # --------------------------------------------------------------------------- @@ -190,19 +205,25 @@ done systemctl daemon-reload # --------------------------------------------------------------------------- -# 10. Firewall (deny-by-default posture) +# 10. Firewall (deny-by-default, explicit allow for Tailscale) # --------------------------------------------------------------------------- log "Configuring firewall..." if ! ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow 22/tcp ufw default deny incoming ufw default allow outgoing + + # Allow all traffic on the Tailscale interface (if present) + # This lets the agent and engine ports be reached over the private overlay. + ufw allow in on tailscale0 || warn "tailscale0 not present yet; rule will activate when interface appears" + ufw --force enable - log "Firewall enabled: SSH (22) allowed, all other incoming denied." - warn "Agent and RivetKit ports are NOT exposed publicly." - warn "Access is via Tailscale or direct private IP only." + log "Firewall enabled: SSH (22) allowed, tailscale0 allowed." + warn "Agent and RivetKit ports are NOT exposed on public interfaces." + warn "Reachability is via Tailscale (tailscale0) only." else - log "Firewall already active." + log "Firewall already active. Ensuring tailscale0 rule..." + ufw allow in on tailscale0 2>/dev/null || true fi # --------------------------------------------------------------------------- @@ -217,15 +238,22 @@ if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then --hostname "${TAILSCALE_HOSTNAME:-zopu-runtime}" \ --accept-routes log "Tailscale configured: $(tailscale ip -4 2>/dev/null || echo 'waiting for IP')" + + # Re-apply the tailscale0 firewall rule now that the interface exists + ufw allow in on tailscale0 2>/dev/null || true else warn "TAILSCALE_AUTHKEY not set — skipping Tailscale setup." + warn "Without Tailscale, services are reachable only via localhost." + warn "To use a private network interface, add an explicit UFW rule:" + warn " ufw allow in on " fi # --------------------------------------------------------------------------- # 12. Disk-space monitoring cron +# /etc/cron.d format REQUIRES a username field. # --------------------------------------------------------------------------- log "Installing disk-space monitor (daily at 06:00)..." -CRON_LINE="0 6 * * * $DEPLOY_DIR/scripts/disk-monitor.sh --warn-percent 80 >> /var/log/zopu/disk-monitor.log 2>&1" +CRON_LINE="0 6 * * * ${SERVICE_USER} ${DEPLOY_DIR}/scripts/disk-monitor.sh --warn-percent 80 >> /var/log/zopu/disk-monitor.log 2>&1" echo "$CRON_LINE" > /etc/cron.d/zopu-disk-monitor chmod 644 /etc/cron.d/zopu-disk-monitor diff --git a/deploy/zopu-runtime/scripts/rollback.sh b/deploy/zopu-runtime/scripts/rollback.sh index fe2f09d..33cc1eb 100755 --- a/deploy/zopu-runtime/scripts/rollback.sh +++ b/deploy/zopu-runtime/scripts/rollback.sh @@ -8,6 +8,8 @@ # # .env is gitignored and is never touched by git operations. It survives # updates and rollbacks unchanged. +# +# Must be run as root (uses runuser to build as the service user). set -euo pipefail @@ -21,6 +23,10 @@ NC='\033[0m' log() { echo -e "${GREEN}[rollback]${NC} $*"; } err() { echo -e "${RED}[rollback]${NC} $*" >&2; } +run_as_service() { + runuser -u "$SERVICE_USER" -- "$@" +} + cd "$INSTALL_DIR" # Determine target @@ -51,21 +57,26 @@ echo "$CURRENT_SHA" > "${INSTALL_DIR}/.pre-rollback-sha" # Checkout target git checkout "$ROLLBACK_SHA" -# Confirm .env is intact +# Restore ownership of the checkout to the service user after git operations +log "Setting ownership of checkout to $SERVICE_USER..." +chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR" + +# Confirm .env is intact and has correct permissions if [[ -f "${INSTALL_DIR}/.env" ]]; then log ".env preserved." + chmod 600 "${INSTALL_DIR}/.env" else err ".env is missing! Restore it from backup before starting services." fi log "Running bun install..." -sudo -u "$SERVICE_USER" bun install +run_as_service bun install log "Building daemon..." -sudo -u "$SERVICE_USER" bun run build:daemon +run_as_service bun run build:daemon log "Building agent service..." -sudo -u "$SERVICE_USER" bun run build:agents +run_as_service bun run build:agents log "Restarting services..." systemctl restart zopu-daemon diff --git a/deploy/zopu-runtime/scripts/update.sh b/deploy/zopu-runtime/scripts/update.sh index bffcd71..bd1ac01 100755 --- a/deploy/zopu-runtime/scripts/update.sh +++ b/deploy/zopu-runtime/scripts/update.sh @@ -12,6 +12,8 @@ # # .env is gitignored and is never touched by git operations. It survives # updates and rollbacks unchanged. +# +# Must be run as root (uses runuser to build as the service user). set -euo pipefail @@ -28,6 +30,10 @@ log() { echo -e "${GREEN}[update]${NC} $*"; } warn() { echo -e "${YELLOW}[update]${NC} $*"; } err() { echo -e "${RED}[update]${NC} $*" >&2; } +run_as_service() { + runuser -u "$SERVICE_USER" -- "$@" +} + cd "$INSTALL_DIR" # Record current commit for rollback @@ -55,22 +61,27 @@ fi NEW_SHA=$(git rev-parse HEAD) log "Now at: ${NEW_SHA:0:12}" -# Confirm .env is intact +# Restore ownership of the checkout to the service user after git operations +log "Setting ownership of checkout to $SERVICE_USER..." +chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR" + +# Confirm .env is intact and has correct permissions if [[ -f "${INSTALL_DIR}/.env" ]]; then log ".env preserved." + chmod 600 "${INSTALL_DIR}/.env" else err ".env is missing! Restore it from backup before starting services." fi # Install and build log "Running bun install..." -sudo -u "$SERVICE_USER" bun install +run_as_service bun install log "Building daemon binary..." -sudo -u "$SERVICE_USER" bun run build:daemon +run_as_service bun run build:daemon log "Building agent service..." -sudo -u "$SERVICE_USER" bun run build:agents +run_as_service bun run build:agents # Graceful restart: daemon first (owns RivetKit engine), then agent log "Restarting services..." diff --git a/deploy/zopu-runtime/systemd/zopu-agent.service b/deploy/zopu-runtime/systemd/zopu-agent.service index 65a0837..170d58a 100644 --- a/deploy/zopu-runtime/systemd/zopu-agent.service +++ b/deploy/zopu-runtime/systemd/zopu-agent.service @@ -32,5 +32,8 @@ ProtectSystem=full ProtectHome=true PrivateTmp=true +# Docker socket access for Orb sandbox runtime (lives in agent process) +SupplementaryGroups=docker + [Install] WantedBy=multi-user.target diff --git a/deploy/zopu-runtime/systemd/zopu-health.service b/deploy/zopu-runtime/systemd/zopu-health.service index 567494d..eeaca41 100644 --- a/deploy/zopu-runtime/systemd/zopu-health.service +++ b/deploy/zopu-runtime/systemd/zopu-health.service @@ -5,4 +5,7 @@ Description=Zopu Health Check Type=oneshot User=__SERVICE_USER__ EnvironmentFile=__INSTALL_DIR__/.env +# health-check.sh reads ENV_FILE to find the .env to source. Set it to the +# install path so custom install dirs work correctly. +Environment=ENV_FILE=__INSTALL_DIR__/.env ExecStart=__INSTALL_DIR__/deploy/zopu-runtime/scripts/health-check.sh --quiet