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