fix: deployment corrections from independent review

Six fixes addressing operational correctness on a real Debian host:

1. Ownership after git operations: chown -R zopu:zopu on the checkout
   after clone/update/rollback before running bun install/build as the
   service user. .env kept at 0600 with explicit chmod after each
   operation.

2. sudo replaced with runuser: minimal Debian does not include sudo.
   runuser is part of util-linux (essential) and always available.
   All three scripts (bootstrap, update, rollback) now use runuser -u.

3. cron.d entry fixed: /etc/cron.d format requires a username field.
   Added ${SERVICE_USER} between the time fields and the command path.

4. Firewall: added explicit `ufw allow in on tailscale0` rule so the
   deny-incoming default does not block Tailscale private-overlay
   reachability. Removed inaccurate claim that direct private IP works
   by default; documented that an explicit per-interface rule is needed.

5. SupplementaryGroups=docker added to zopu-agent.service: the Orb
   sandbox runtime lives in the agent process, not just the daemon.

6. zopu-health.service: added Environment=ENV_FILE=__INSTALL_DIR__/.env
   so health-check.sh sources the correct .env at custom install paths.

Verified Flue build output: bun run build:agents produces
packages/agents/dist/server.mjs (confirmed by running the build).
Agent unit ExecStart path is correct.

Validated: bash -n on all 6 shell scripts; mocked health-check smoke
against live RivetKit engine on port 6420; systemd unit structural
checks. No JS/TS/agent files changed.
This commit is contained in:
-Puter
2026-07-24 20:49:54 +05:30
parent 8f0b915f4b
commit 113c755e23
6 changed files with 90 additions and 25 deletions

View File

@@ -33,7 +33,7 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
│ │ │ │
│ systemd timers: health-check (60s), docker-cleanup (daily) │ │ systemd timers: health-check (60s), docker-cleanup (daily) │
│ cron: disk-monitor (daily 06:00) │ │ cron: disk-monitor (daily 06:00) │
│ ufw: deny-by-default, SSH only │ ufw: deny-incoming, SSH + tailscale0
│ Tailscale: optional private overlay │ │ Tailscale: optional private overlay │
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
@@ -265,16 +265,25 @@ Default alert threshold is 80%.
## Firewall and private networking ## Firewall and private networking
The firewall (`ufw`) is deny-by-default: 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 - All other incoming traffic is denied
The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** 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. `TAILSCALE_AUTHKEY` before running bootstrap to configure automatically.
2. **Direct private IP**: if the server is on a private network, the ports Other Tailscale-connected machines can reach the agent at
are reachable from other machines on the same subnet. `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 3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in
`caddy/` if you need TLS termination or a public ingress point. `caddy/` if you need TLS termination or a public ingress point.

View File

@@ -4,7 +4,7 @@
# #
# Run as root on a fresh Debian 12 host: # Run as root on a fresh Debian 12 host:
# #
# sudo bash bootstrap.sh # bash bootstrap.sh
# #
# Environment overrides (set before running): # Environment overrides (set before running):
# ZOPU_REPO_URL — SSH clone URL (default: ssh://git@git.openputer.com:2222/puter/zopu-code.git) # 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} $*"; } warn() { echo -e "${YELLOW}[bootstrap]${NC} $*"; }
err() { echo -e "${RED}[bootstrap]${NC} $*" >&2; } 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 # Pre-flight
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -64,9 +70,9 @@ apt-get install -y \
ufw \ ufw \
git \ git \
jq \ jq \
netcat-openbsd \
openssh-client \ openssh-client \
unattended-upgrades \ unattended-upgrades \
netcat-openbsd \
rsyslog rsyslog
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -137,17 +143,23 @@ else
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
fi 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 # 6. Install dependencies and build
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Running bun install..." log "Running bun install..."
sudo -u "$SERVICE_USER" bun install run_as_service bun install
log "Building daemon binary..." log "Building daemon binary..."
sudo -u "$SERVICE_USER" bun run build:daemon run_as_service bun run build:daemon
log "Building agent service..." log "Building agent service..."
sudo -u "$SERVICE_USER" bun run build:agents run_as_service bun run build:agents
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 7. Environment file # 7. Environment file
@@ -161,6 +173,9 @@ if [[ ! -f "$ENV_FILE" ]]; then
warn "Edit $ENV_FILE with real values before starting services." warn "Edit $ENV_FILE with real values before starting services."
else else
log ".env already exists at $ENV_FILE" 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 fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -190,19 +205,25 @@ done
systemctl daemon-reload systemctl daemon-reload
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 10. Firewall (deny-by-default posture) # 10. Firewall (deny-by-default, explicit allow for Tailscale)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Configuring firewall..." log "Configuring firewall..."
if ! ufw status 2>/dev/null | grep -q "Status: active"; then if ! ufw status 2>/dev/null | grep -q "Status: active"; then
ufw allow 22/tcp ufw allow 22/tcp
ufw default deny incoming ufw default deny incoming
ufw default allow outgoing 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 ufw --force enable
log "Firewall enabled: SSH (22) allowed, all other incoming denied." log "Firewall enabled: SSH (22) allowed, tailscale0 allowed."
warn "Agent and RivetKit ports are NOT exposed publicly." warn "Agent and RivetKit ports are NOT exposed on public interfaces."
warn "Access is via Tailscale or direct private IP only." warn "Reachability is via Tailscale (tailscale0) only."
else else
log "Firewall already active." log "Firewall already active. Ensuring tailscale0 rule..."
ufw allow in on tailscale0 2>/dev/null || true
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -217,15 +238,22 @@ if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then
--hostname "${TAILSCALE_HOSTNAME:-zopu-runtime}" \ --hostname "${TAILSCALE_HOSTNAME:-zopu-runtime}" \
--accept-routes --accept-routes
log "Tailscale configured: $(tailscale ip -4 2>/dev/null || echo 'waiting for IP')" 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 else
warn "TAILSCALE_AUTHKEY not set — skipping Tailscale setup." 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 <interface>"
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 12. Disk-space monitoring cron # 12. Disk-space monitoring cron
# /etc/cron.d format REQUIRES a username field.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Installing disk-space monitor (daily at 06:00)..." 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 echo "$CRON_LINE" > /etc/cron.d/zopu-disk-monitor
chmod 644 /etc/cron.d/zopu-disk-monitor chmod 644 /etc/cron.d/zopu-disk-monitor

View File

@@ -8,6 +8,8 @@
# #
# .env is gitignored and is never touched by git operations. It survives # .env is gitignored and is never touched by git operations. It survives
# updates and rollbacks unchanged. # updates and rollbacks unchanged.
#
# Must be run as root (uses runuser to build as the service user).
set -euo pipefail set -euo pipefail
@@ -21,6 +23,10 @@ NC='\033[0m'
log() { echo -e "${GREEN}[rollback]${NC} $*"; } log() { echo -e "${GREEN}[rollback]${NC} $*"; }
err() { echo -e "${RED}[rollback]${NC} $*" >&2; } err() { echo -e "${RED}[rollback]${NC} $*" >&2; }
run_as_service() {
runuser -u "$SERVICE_USER" -- "$@"
}
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
# Determine target # Determine target
@@ -51,21 +57,26 @@ echo "$CURRENT_SHA" > "${INSTALL_DIR}/.pre-rollback-sha"
# Checkout target # Checkout target
git checkout "$ROLLBACK_SHA" 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 if [[ -f "${INSTALL_DIR}/.env" ]]; then
log ".env preserved." log ".env preserved."
chmod 600 "${INSTALL_DIR}/.env"
else else
err ".env is missing! Restore it from backup before starting services." err ".env is missing! Restore it from backup before starting services."
fi fi
log "Running bun install..." log "Running bun install..."
sudo -u "$SERVICE_USER" bun install run_as_service bun install
log "Building daemon..." log "Building daemon..."
sudo -u "$SERVICE_USER" bun run build:daemon run_as_service bun run build:daemon
log "Building agent service..." log "Building agent service..."
sudo -u "$SERVICE_USER" bun run build:agents run_as_service bun run build:agents
log "Restarting services..." log "Restarting services..."
systemctl restart zopu-daemon systemctl restart zopu-daemon

View File

@@ -12,6 +12,8 @@
# #
# .env is gitignored and is never touched by git operations. It survives # .env is gitignored and is never touched by git operations. It survives
# updates and rollbacks unchanged. # updates and rollbacks unchanged.
#
# Must be run as root (uses runuser to build as the service user).
set -euo pipefail set -euo pipefail
@@ -28,6 +30,10 @@ log() { echo -e "${GREEN}[update]${NC} $*"; }
warn() { echo -e "${YELLOW}[update]${NC} $*"; } warn() { echo -e "${YELLOW}[update]${NC} $*"; }
err() { echo -e "${RED}[update]${NC} $*" >&2; } err() { echo -e "${RED}[update]${NC} $*" >&2; }
run_as_service() {
runuser -u "$SERVICE_USER" -- "$@"
}
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
# Record current commit for rollback # Record current commit for rollback
@@ -55,22 +61,27 @@ fi
NEW_SHA=$(git rev-parse HEAD) NEW_SHA=$(git rev-parse HEAD)
log "Now at: ${NEW_SHA:0:12}" 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 if [[ -f "${INSTALL_DIR}/.env" ]]; then
log ".env preserved." log ".env preserved."
chmod 600 "${INSTALL_DIR}/.env"
else else
err ".env is missing! Restore it from backup before starting services." err ".env is missing! Restore it from backup before starting services."
fi fi
# Install and build # Install and build
log "Running bun install..." log "Running bun install..."
sudo -u "$SERVICE_USER" bun install run_as_service bun install
log "Building daemon binary..." log "Building daemon binary..."
sudo -u "$SERVICE_USER" bun run build:daemon run_as_service bun run build:daemon
log "Building agent service..." 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 # Graceful restart: daemon first (owns RivetKit engine), then agent
log "Restarting services..." log "Restarting services..."

View File

@@ -32,5 +32,8 @@ ProtectSystem=full
ProtectHome=true ProtectHome=true
PrivateTmp=true PrivateTmp=true
# Docker socket access for Orb sandbox runtime (lives in agent process)
SupplementaryGroups=docker
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -5,4 +5,7 @@ Description=Zopu Health Check
Type=oneshot Type=oneshot
User=__SERVICE_USER__ User=__SERVICE_USER__
EnvironmentFile=__INSTALL_DIR__/.env 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 ExecStart=__INSTALL_DIR__/deploy/zopu-runtime/scripts/health-check.sh --quiet