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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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 <interface>"
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user