Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 104 additions & 8 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ apply_model_override() {
return 1
fi

local model_override="$NEMOCLAW_MODEL_OVERRIDE"
local model_override="${NEMOCLAW_MODEL_OVERRIDE:-}"
local api_override="${NEMOCLAW_INFERENCE_API_OVERRIDE:-}"

# SECURITY: Validate inputs — reject control characters and enforce length limit.
Expand Down Expand Up @@ -401,6 +401,86 @@ PYCORS
printf '[config] Config hash recomputed after CORS override\n' >&2
}

# ── Slack token placeholder resolution ────────────────────────────
# Resolves openshell:resolve:env:SLACK_* placeholders in openclaw.json at
# container startup, before chattr +i locks the file. This ensures Bolt's
# in-process token validation (appToken must start with xapp-) succeeds even
# before the L7 proxy can intercept HTTP calls.
# Same trust model as apply_model_override: host-set env vars, root-only,
# applied before Landlock/chattr +i, hash recomputed. Tokens are unset from
# the process env after patching so they are not visible inside the sandbox.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/2085

apply_slack_token_override() {
[ -n "${SLACK_BOT_TOKEN:-}" ] || return 0

# SECURITY: Only root can write to /sandbox/.openclaw (root:root 444).
if [ "$(id -u)" -ne 0 ]; then
printf '[SECURITY] Slack token override ignored — requires root (non-root mode cannot write to config)\n' >&2
return 0
fi

local config_file="/sandbox/.openclaw/openclaw.json"
local hash_file="/sandbox/.openclaw/.config-hash"

# SECURITY: Refuse to write through symlinks to prevent symlink-following attacks.
if [ -L "$config_file" ] || [ -L "$hash_file" ]; then
printf '[SECURITY] Refusing Slack token override — config or hash path is a symlink\n' >&2
return 1
fi

# SECURITY: Validate token prefixes — reject anything that doesn't look like a real Slack token.
case "${SLACK_BOT_TOKEN}" in
xoxb-*) ;;
*)
printf '[channels] SLACK_BOT_TOKEN does not start with xoxb- — skipping Slack placeholder resolution\n' >&2
return 0
;;
esac

if [ -n "${SLACK_APP_TOKEN:-}" ]; then
case "$SLACK_APP_TOKEN" in
xapp-*) ;;
*)
printf '[channels] SLACK_APP_TOKEN does not start with xapp- — skipping Slack placeholder resolution\n' >&2
return 0
;;
esac
else
printf '[channels] Warning: SLACK_BOT_TOKEN is set but SLACK_APP_TOKEN is missing — Socket Mode requires both tokens\n' >&2
fi

printf '[channels] Resolving Slack token placeholders in openclaw.json\n' >&2

SLACK_BOT_TOKEN="$SLACK_BOT_TOKEN" \
SLACK_APP_TOKEN="${SLACK_APP_TOKEN:-}" \
python3 - "$config_file" <<'PYSLACK'
import json, os, sys

config_file = sys.argv[1]
bot_token = os.environ["SLACK_BOT_TOKEN"]
app_token = os.environ.get("SLACK_APP_TOKEN", "")
placeholder_prefix = "openshell:resolve:env:"

with open(config_file) as f:
cfg = json.load(f)

slack = cfg.get("channels", {}).get("slack", {})
default_acct = slack.get("accounts", {}).get("default", {})

if default_acct.get("botToken", "").startswith(placeholder_prefix):
default_acct["botToken"] = bot_token
if app_token and default_acct.get("appToken", "").startswith(placeholder_prefix):
default_acct["appToken"] = app_token

with open(config_file, "w") as f:
json.dump(cfg, f, indent=2)
PYSLACK

(cd /sandbox/.openclaw && sha256sum openclaw.json >"$hash_file")
printf '[channels] Config hash recomputed after Slack token override\n' >&2
}

_read_gateway_token() {
python3 - <<'PYTOKEN'
import json
Expand Down Expand Up @@ -635,14 +715,16 @@ harden_auth_profiles() {

configure_messaging_channels() {
# Channel entries are baked into openclaw.json at image build time via
# NEMOCLAW_MESSAGING_CHANNELS_B64 (see Dockerfile). Placeholder tokens
# (openshell:resolve:env:*) flow through to API calls where the L7 proxy
# rewrites them with real secrets at egress. Real tokens are never visible
# inside the sandbox.
# NEMOCLAW_MESSAGING_CHANNELS_B64 (see Dockerfile).
#
# Telegram/Discord: placeholder tokens (openshell:resolve:env:*) flow through
# to API calls where the L7 proxy rewrites them with real secrets at egress.
# Real tokens are never visible inside the sandbox for these channels.
#
# Runtime patching of /sandbox/.openclaw/openclaw.json is not possible:
# Landlock enforces read-only on /sandbox/.openclaw/ at the kernel level,
# regardless of DAC (file ownership/chmod). Writes fail with EPERM.
# Slack: apply_slack_token_override (runs before this function) resolves
# SLACK_BOT_TOKEN/SLACK_APP_TOKEN placeholders directly into openclaw.json so
# Bolt's in-process token validation passes. Both env vars are unset before the
# gateway starts (root path) so they do not leak into the sandbox process env.
[ -n "${TELEGRAM_BOT_TOKEN:-}" ] || [ -n "${DISCORD_BOT_TOKEN:-}" ] || [ -n "${SLACK_BOT_TOKEN:-}" ] || return 0

echo "[channels] Messaging channels active (baked at build time):" >&2
Expand Down Expand Up @@ -876,6 +958,14 @@ if [ "$(id -u)" -ne 0 ]; then
fi
apply_model_override
apply_cors_override
apply_slack_token_override
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# SECURITY: apply_slack_token_override is a no-op when non-root.
# If SLACK_BOT_TOKEN is still set here the placeholder was never resolved —
# Bolt will crash with invalid_auth at startup. Fail fast with a clear message.
if [ -n "${SLACK_BOT_TOKEN:-}" ]; then
printf '[SECURITY] Slack Socket Mode requires a root container — SLACK_BOT_TOKEN is set but token placeholder resolution needs root. Run the container as root or remove SLACK_BOT_TOKEN.\n' >&2
exit 1
fi
export_gateway_token
install_configure_guard
configure_messaging_channels
Expand Down Expand Up @@ -975,6 +1065,7 @@ fi
verify_config_integrity
apply_model_override
apply_cors_override
apply_slack_token_override
export_gateway_token
install_configure_guard

Expand All @@ -983,6 +1074,11 @@ install_configure_guard
# BEFORE chattr +i (which locks the config permanently).
configure_messaging_channels

# SECURITY: Slack tokens were resolved into openclaw.json by apply_slack_token_override.
# Unset here — before any gosu sandbox child — so neither the sandbox user nor
# the gateway inherits them from the process environment.
unset SLACK_BOT_TOKEN SLACK_APP_TOKEN

# Write auth profile as sandbox user (needs writable .openclaw-data)
# and recursively re-tighten any auth-profiles.json files under ~/.openclaw.
gosu sandbox bash -c "$(declare -f write_auth_profile harden_auth_profiles); write_auth_profile; harden_auth_profiles"
Expand Down
Loading
Loading