refactor(entrypoint): improve privilege dropping and config initialization
- Updated entrypoint.sh to initialize the on-disk config only if it does not already exist, preserving user edits across restarts. - Enhanced privilege dropping logic to ensure the container does not run as root if the privilege drop fails. - Clarified comments in Dockerfile and entrypoint.sh for better understanding of the privilege management process. - Updated README.md to include a note about persistent disks requiring a paid service on Render. - Adjusted render.yaml to clarify the Docker command behavior and added a note regarding auto-deploy settings.
This commit is contained in:
+5
-3
@@ -42,9 +42,11 @@ RUN useradd -m -u 1000 -s /bin/bash nanobot && \
|
|||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
|
RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
# Start as root so the Render entrypoint can chown the freshly-mounted
|
# Start as root so the entrypoint can chown the data dir (on Render, the
|
||||||
# (root-owned) persistent disk, then drop to the non-root nanobot user via
|
# freshly-mounted root-owned persistent disk) before dropping to the non-root
|
||||||
# setpriv (see entrypoint.sh). Local runs without a disk are unaffected.
|
# nanobot user via setpriv. The entrypoint drops privileges on every root start
|
||||||
|
# and fails closed if it cannot, so the agent never runs as root (see
|
||||||
|
# entrypoint.sh).
|
||||||
USER root
|
USER root
|
||||||
ENV HOME=/home/nanobot
|
ENV HOME=/home/nanobot
|
||||||
# Ensure crash output reaches Render logs (app output is otherwise swallowed on
|
# Ensure crash output reaches Render logs (app output is otherwise swallowed on
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
|
|
||||||
Deploy nanobot's gateway and bundled WebUI as a single web service with persistent memory. Render reads [`render.yaml`](./render.yaml) and prompts for two secrets on deploy: `ANTHROPIC_API_KEY` and `NANOBOT_WEB_TOKEN` (the password that gates the public WebUI — generate a strong random value, e.g. `openssl rand -hex 32`).
|
Deploy nanobot's gateway and bundled WebUI as a single web service with persistent memory. Render reads [`render.yaml`](./render.yaml) and prompts for two secrets on deploy: `ANTHROPIC_API_KEY` and `NANOBOT_WEB_TOKEN` (the password that gates the public WebUI — generate a strong random value, e.g. `openssl rand -hex 32`).
|
||||||
|
|
||||||
|
> **Note:** The blueprint attaches a persistent disk so sessions, memory, and WebUI history survive restarts. Persistent disks require a paid service (they are not available on Render's free tier).
|
||||||
|
|
||||||
[](https://render.com/deploy?repo=https://github.com/HKUDS/nanobot)
|
[](https://render.com/deploy?repo=https://github.com/HKUDS/nanobot)
|
||||||
|
|
||||||
## What can nanobot do?
|
## What can nanobot do?
|
||||||
|
|||||||
+30
-13
@@ -3,26 +3,42 @@ dir="$HOME/.nanobot"
|
|||||||
|
|
||||||
# Render deploy path (see render.yaml + render-config.json). Gated on Render's
|
# Render deploy path (see render.yaml + render-config.json). Gated on Render's
|
||||||
# automatic RENDER=true env var so local Docker/podman usage is unaffected.
|
# automatic RENDER=true env var so local Docker/podman usage is unaffected.
|
||||||
# Copies the committed config template onto the mounted disk (wiring secrets via
|
# Initializes the on-disk config from the committed template (wiring secrets via
|
||||||
# ${VAR} env vars and keeping runtime data on the persistent disk), chowns the
|
# ${VAR} env vars, keeping runtime data on the persistent disk) and appends the
|
||||||
# root-owned mount, then drops to the non-root nanobot user. Logs each decision
|
# --config flag. Logs each decision so a failed start is diagnosable in Render's
|
||||||
# so a failed start is diagnosable in Render's logs.
|
# logs. Privilege dropping is handled below, for every root start (not just here).
|
||||||
if [ "$RENDER" = "true" ]; then
|
if [ "$RENDER" = "true" ]; then
|
||||||
echo "[entrypoint] Render deploy — starting as $(id)"
|
echo "[entrypoint] Render deploy — starting as $(id)"
|
||||||
mkdir -p "$dir" || echo "[entrypoint] warning: mkdir $dir failed"
|
mkdir -p "$dir" || echo "[entrypoint] warning: mkdir $dir failed"
|
||||||
config="$dir/config.json"
|
config="$dir/config.json"
|
||||||
cp /app/render-config.json "$config" || echo "[entrypoint] warning: cp config failed"
|
# Initialize config only when it does not already exist, so WebUI/provider
|
||||||
if [ "$(id -u)" = "0" ]; then
|
# settings edited at runtime survive restarts. The disk persists config.json
|
||||||
chown -R nanobot:nanobot "$dir" || echo "[entrypoint] warning: chown $dir failed"
|
# across deploys; overwriting it every boot would discard those changes.
|
||||||
if setpriv --reuid=nanobot --regid=nanobot --init-groups true 2>/dev/null; then
|
if [ ! -f "$config" ]; then
|
||||||
echo "[entrypoint] dropping privileges to nanobot via setpriv"
|
echo "[entrypoint] initializing $config from render-config.json"
|
||||||
exec setpriv --reuid=nanobot --regid=nanobot --init-groups nanobot "$@" --config "$config"
|
cp /app/render-config.json "$config" || echo "[entrypoint] warning: cp config failed"
|
||||||
fi
|
else
|
||||||
echo "[entrypoint] setpriv privilege-drop not permitted — running as root"
|
echo "[entrypoint] existing $config found — leaving it in place"
|
||||||
fi
|
fi
|
||||||
exec nanobot "$@" --config "$config"
|
set -- "$@" --config "$config"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Drop privileges whenever the container starts as root. Render mounts the
|
||||||
|
# persistent disk root-owned, and a plain `docker run` also defaults to root now,
|
||||||
|
# so this covers both. Chown the data dir so the non-root user can write it, then
|
||||||
|
# re-exec as nanobot. Fail closed: if the privilege drop cannot be performed,
|
||||||
|
# exit rather than run the agent as root.
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
chown -R nanobot:nanobot "$dir" 2>/dev/null || echo "[entrypoint] warning: chown $dir failed"
|
||||||
|
if setpriv --reuid=nanobot --regid=nanobot --init-groups true 2>/dev/null; then
|
||||||
|
echo "[entrypoint] dropping privileges to nanobot via setpriv"
|
||||||
|
exec setpriv --reuid=nanobot --regid=nanobot --init-groups nanobot "$@"
|
||||||
|
fi
|
||||||
|
echo "[entrypoint] error: started as root but setpriv privilege drop failed — refusing to run as root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Already non-root: make sure the data dir is writable before starting.
|
||||||
if [ -d "$dir" ] && [ ! -w "$dir" ]; then
|
if [ -d "$dir" ] && [ ! -w "$dir" ]; then
|
||||||
owner_uid=$(stat -c %u "$dir" 2>/dev/null || stat -f %u "$dir" 2>/dev/null)
|
owner_uid=$(stat -c %u "$dir" 2>/dev/null || stat -f %u "$dir" 2>/dev/null)
|
||||||
cat >&2 <<EOF
|
cat >&2 <<EOF
|
||||||
@@ -35,4 +51,5 @@ Fix (pick one):
|
|||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec nanobot "$@"
|
exec nanobot "$@"
|
||||||
|
|||||||
+12
-6
@@ -8,13 +8,18 @@ services:
|
|||||||
runtime: docker
|
runtime: docker
|
||||||
dockerfilePath: ./Dockerfile
|
dockerfilePath: ./Dockerfile
|
||||||
dockerContext: .
|
dockerContext: .
|
||||||
# Render's Docker Command REPLACES the Dockerfile ENTRYPOINT (it is not
|
# Render's Docker Command REPLACES the Dockerfile CMD but keeps the
|
||||||
# appended to it), so invoke the entrypoint explicitly. When RENDER=true the
|
# ENTRYPOINT (entrypoint.sh), so pass only the command args here — otherwise
|
||||||
# entrypoint copies render-config.json onto the mounted disk at
|
# the entrypoint path is reinjected as an argument and nanobot fails with
|
||||||
|
# "No such command /usr/local/bin/entrypoint.sh". When RENDER=true the
|
||||||
|
# entrypoint initializes render-config.json on the mounted disk at
|
||||||
# $HOME/.nanobot/config.json (so the runtime data_dir lands on the persistent
|
# $HOME/.nanobot/config.json (so the runtime data_dir lands on the persistent
|
||||||
# disk), chowns the root-owned mount, drops to the non-root nanobot user, and
|
# disk), chowns the root-owned mount, drops to the non-root nanobot user, and
|
||||||
# appends the --config flag itself — so pass only `gateway` here.
|
# appends the --config flag itself.
|
||||||
dockerCommand: /usr/local/bin/entrypoint.sh gateway
|
dockerCommand: gateway
|
||||||
|
# Deploy-to-Render templates should not auto-deploy on every repo push
|
||||||
|
# (Render's recommendation); trigger deploys manually or via the Dashboard.
|
||||||
|
autoDeploy: false
|
||||||
plan: starter
|
plan: starter
|
||||||
healthCheckPath: /
|
healthCheckPath: /
|
||||||
envVars:
|
envVars:
|
||||||
@@ -35,7 +40,8 @@ services:
|
|||||||
# data_dir (config_path.parent) resolves here too — keeping webui/ (chat
|
# data_dir (config_path.parent) resolves here too — keeping webui/ (chat
|
||||||
# history the UI renders), cron, media, and logs durable, not just session
|
# history the UI renders), cron, media, and logs durable, not just session
|
||||||
# files. Starter / 1 GB is the lean default; history persistence needs this
|
# files. Starter / 1 GB is the lean default; history persistence needs this
|
||||||
# disk, not a bigger plan.
|
# disk, not a bigger plan. Note: persistent disks require a paid service
|
||||||
|
# (they are unavailable on Render's free tier).
|
||||||
disk:
|
disk:
|
||||||
name: nanobot-data
|
name: nanobot-data
|
||||||
mountPath: /home/nanobot/.nanobot
|
mountPath: /home/nanobot/.nanobot
|
||||||
|
|||||||
Reference in New Issue
Block a user