#!/bin/bash # ============================================================================= # OpenClaw Security Validator # ============================================================================= # Run this script AFTER installing OpenClaw to verify your setup is secure. # Works on both macOS and Linux/WSL2. # # Usage: # bash validate-security.sh # # Output: # ✅ = Secure (green) # ❌ = Insecure — action required (red) # ⚠️ = Warning — review recommended (yellow) # ℹ️ = Info (blue) # ============================================================================= set -euo pipefail # --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' # --- Counters --- PASS=0 FAIL=0 WARN=0 RECOMMENDATIONS=() # --- Helpers --- pass() { echo -e " ${GREEN}✅ $1${NC}" ((PASS++)) } fail() { echo -e " ${RED}❌ $1${NC}" ((FAIL++)) RECOMMENDATIONS+=("$2") } warn() { echo -e " ${YELLOW}⚠️ $1${NC}" ((WARN++)) RECOMMENDATIONS+=("$2") } info() { echo -e " ${BLUE}ℹ️ $1${NC}" } section() { echo "" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BOLD} $1${NC}" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } # --- Detect OS --- OS="unknown" if [[ "$(uname -s)" == "Darwin" ]]; then OS="macos" elif grep -qi microsoft /proc/version 2>/dev/null; then OS="wsl2" elif [[ "$(uname -s)" == "Linux" ]]; then OS="linux" fi OPENCLAW_DIR="$HOME/.openclaw" CONFIG_FILE="$OPENCLAW_DIR/config.yaml" # ============================================================================= echo "" echo -e "${BOLD}🦞 OpenClaw Security Validator${NC}" echo -e " $(date '+%Y-%m-%d %H:%M:%S')" echo -e " Platform: ${BOLD}$OS${NC}" echo "" # ============================================================================= section "1. OpenClaw Installation" # ============================================================================= # Check if openclaw is installed if command -v openclaw &>/dev/null; then VERSION=$(openclaw --version 2>/dev/null || echo "unknown") pass "OpenClaw installed: $VERSION" else fail "OpenClaw is NOT installed" \ "Install OpenClaw: npm install -g openclaw@latest && openclaw onboard --install-daemon" fi # Check Node.js version if command -v node &>/dev/null; then NODE_VER=$(node --version 2>/dev/null | sed 's/v//') NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) if [[ "$NODE_MAJOR" -ge 22 ]]; then pass "Node.js version: v$NODE_VER (>= 22 required)" else fail "Node.js version: v$NODE_VER (NEEDS >= 22)" \ "Upgrade Node.js: nvm install 22 && nvm alias default 22" fi else fail "Node.js is NOT installed" \ "Install Node.js 22+: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash && nvm install 22" fi # Check config exists if [[ -f "$CONFIG_FILE" ]]; then pass "Config file exists: $CONFIG_FILE" else fail "Config file NOT found at $CONFIG_FILE" \ "Run: openclaw onboard --install-daemon" fi # ============================================================================= section "2. Gateway Configuration" # ============================================================================= if [[ -f "$CONFIG_FILE" ]]; then # Check bind address BIND_ADDR=$(grep -E '^\s*bind:' "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*bind:\s*//' | tr -d '"' | tr -d "'" | xargs) if [[ -z "$BIND_ADDR" ]]; then warn "Gateway bind address not explicitly set (may default to 0.0.0.0)" \ "Add to config.yaml under gateway: bind: \"127.0.0.1\"" elif [[ "$BIND_ADDR" == "127.0.0.1" || "$BIND_ADDR" == "localhost" ]]; then pass "Gateway binds to loopback only: $BIND_ADDR" elif [[ "$BIND_ADDR" == "0.0.0.0" ]]; then fail "Gateway binds to ALL interfaces (0.0.0.0) — EXPOSED TO NETWORK!" \ "CRITICAL: Change gateway.bind to \"127.0.0.1\" in $CONFIG_FILE immediately" else warn "Gateway binds to: $BIND_ADDR — verify this is intentional" \ "Recommended: Set gateway.bind to \"127.0.0.1\" unless you have a specific reason" fi # Check auth mode AUTH_MODE=$(grep -E '^\s*mode:' "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*mode:\s*//' | tr -d '"' | tr -d "'" | xargs) if [[ "$AUTH_MODE" == "password" ]]; then pass "Gateway auth mode: password" # Check password strength PASSWORD=$(grep -E '^\s*password:' "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*password:\s*//' | tr -d '"' | tr -d "'" | xargs) if [[ -z "$PASSWORD" ]]; then fail "Gateway password is EMPTY" \ "Set a strong password: openclaw config set gateway.auth.password \"\$(openssl rand -base64 32)\"" elif [[ ${#PASSWORD} -lt 16 ]]; then warn "Gateway password is short (${#PASSWORD} chars, recommend 20+)" \ "Set stronger password: openclaw config set gateway.auth.password \"\$(openssl rand -base64 32)\"" else pass "Gateway password length: ${#PASSWORD} chars" fi elif [[ -z "$AUTH_MODE" ]]; then fail "Gateway auth mode NOT configured — WebChat/Control UI may be unprotected" \ "Add to config.yaml: gateway.auth.mode: \"password\" and set a strong password" else warn "Gateway auth mode: $AUTH_MODE — verify this is secure" \ "Recommended: Set gateway.auth.mode to \"password\"" fi # Check DM policy DM_POLICY=$(grep -E '^\s*dmPolicy:' "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*dmPolicy:\s*//' | tr -d '"' | tr -d "'" | xargs) if [[ "$DM_POLICY" == "pairing" ]]; then pass "DM policy: pairing (unknown senders must be approved)" elif [[ "$DM_POLICY" == "open" ]]; then fail "DM policy is OPEN — ANYONE can message your bot!" \ "CRITICAL: Change dmPolicy to \"pairing\" in $CONFIG_FILE immediately" elif [[ -z "$DM_POLICY" ]]; then warn "DM policy not explicitly set (check default behavior)" \ "Explicitly set dmPolicy: \"pairing\" in $CONFIG_FILE" else info "DM policy: $DM_POLICY" fi # Check Tailscale mode TS_MODE=$(grep -A5 "tailscale:" "$CONFIG_FILE" 2>/dev/null | grep -E '^\s*mode:' | head -1 | sed 's/.*mode:\s*//' | tr -d '"' | tr -d "'" | xargs) if [[ "$TS_MODE" == "serve" ]]; then pass "Tailscale mode: serve (tailnet-only, not public)" elif [[ "$TS_MODE" == "funnel" ]]; then fail "Tailscale mode is FUNNEL — Gateway is publicly accessible!" \ "Change tailscale.mode to \"serve\" unless you absolutely need public access. If you do, ensure gateway.auth.mode is \"password\" with a strong password." elif [[ -z "$TS_MODE" || "$TS_MODE" == "off" ]]; then pass "Tailscale mode: off/unset (no Tailscale exposure)" fi # Check for dangerous tools SYSTEM_RUN=$(grep -A3 "system:" "$CONFIG_FILE" 2>/dev/null | grep -A1 "run:" | grep "enabled:" | sed 's/.*enabled:\s*//' | tr -d ' ' | head -1) if [[ "$SYSTEM_RUN" == "true" ]]; then fail "system.run tool is ENABLED — allows arbitrary command execution!" \ "CRITICAL: Disable system.run in config.yaml: tools.system.run.enabled: false" elif [[ "$SYSTEM_RUN" == "false" ]]; then pass "system.run tool: disabled" else warn "system.run tool status unknown — verify manually" \ "Explicitly set tools.system.run.enabled: false in $CONFIG_FILE" fi BROWSER_ENABLED=$(grep -A2 "browser:" "$CONFIG_FILE" 2>/dev/null | grep "enabled:" | sed 's/.*enabled:\s*//' | tr -d ' ' | head -1) if [[ "$BROWSER_ENABLED" == "true" ]]; then warn "Browser control is ENABLED — agent can browse authenticated sessions" \ "Disable browser unless needed: tools.browser.enabled: false" elif [[ "$BROWSER_ENABLED" == "false" ]]; then pass "Browser control: disabled" else warn "Browser control status unknown — verify manually" \ "Explicitly set tools.browser.enabled: false in $CONFIG_FILE" fi else info "Skipping config checks — config file not found" fi # ============================================================================= section "3. File Permissions" # ============================================================================= if [[ -d "$OPENCLAW_DIR" ]]; then # Check ~/.openclaw directory permissions DIR_PERMS=$(stat -c "%a" "$OPENCLAW_DIR" 2>/dev/null || stat -f "%Lp" "$OPENCLAW_DIR" 2>/dev/null) if [[ "$DIR_PERMS" == "700" ]]; then pass "~/.openclaw/ directory permissions: $DIR_PERMS (owner-only)" else fail "~/.openclaw/ directory permissions: $DIR_PERMS (should be 700)" \ "Fix: chmod 700 ~/.openclaw" fi # Check config file permissions if [[ -f "$CONFIG_FILE" ]]; then FILE_PERMS=$(stat -c "%a" "$CONFIG_FILE" 2>/dev/null || stat -f "%Lp" "$CONFIG_FILE" 2>/dev/null) if [[ "$FILE_PERMS" == "600" ]]; then pass "config.yaml permissions: $FILE_PERMS (owner read/write only)" else fail "config.yaml permissions: $FILE_PERMS (should be 600 — contains API keys!)" \ "Fix: chmod 600 ~/.openclaw/config.yaml" fi fi # Check WhatsApp session directory WA_DIR="$OPENCLAW_DIR/whatsapp" if [[ -d "$WA_DIR" ]]; then WA_PERMS=$(stat -c "%a" "$WA_DIR" 2>/dev/null || stat -f "%Lp" "$WA_DIR" 2>/dev/null) if [[ "$WA_PERMS" == "700" ]]; then pass "WhatsApp session dir permissions: $WA_PERMS" else warn "WhatsApp session dir permissions: $WA_PERMS (should be 700)" \ "Fix: chmod -R 700 ~/.openclaw/whatsapp" fi fi else info "~/.openclaw/ directory not found — skipping permission checks" fi # Check running as root if [[ "$(id -u)" == "0" ]]; then fail "Running as ROOT — never run OpenClaw as root!" \ "Switch to a regular user: su - yourusername" else pass "Not running as root: $(whoami)" fi # ============================================================================= section "4. Network Security" # ============================================================================= # Check if Gateway port is listening GW_PORT=18789 if [[ -f "$CONFIG_FILE" ]]; then CUSTOM_PORT=$(grep -E '^\s*port:' "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/.*port:\s*//' | tr -d ' ') if [[ -n "$CUSTOM_PORT" && "$CUSTOM_PORT" =~ ^[0-9]+$ ]]; then GW_PORT=$CUSTOM_PORT fi fi if command -v ss &>/dev/null; then LISTEN_ADDR=$(ss -tlnp 2>/dev/null | grep ":${GW_PORT}" | awk '{print $4}' | head -1) elif command -v lsof &>/dev/null; then LISTEN_ADDR=$(lsof -iTCP:${GW_PORT} -sTCP:LISTEN -P -n 2>/dev/null | awk 'NR>1{print $9}' | head -1) else LISTEN_ADDR="" fi if [[ -n "$LISTEN_ADDR" ]]; then if echo "$LISTEN_ADDR" | grep -q "127.0.0.1\|localhost\|\[::1\]"; then pass "Gateway listening on loopback only: $LISTEN_ADDR" elif echo "$LISTEN_ADDR" | grep -q "0.0.0.0\|\*:\|\[::\]"; then fail "Gateway listening on ALL interfaces: $LISTEN_ADDR" \ "CRITICAL: Change gateway.bind to \"127.0.0.1\" and restart OpenClaw" else warn "Gateway listening on: $LISTEN_ADDR — verify this is intentional" \ "Recommended: Bind to 127.0.0.1 unless you have a specific reason" fi else info "Gateway not currently running on port $GW_PORT (or cannot detect)" fi # Check SSH if [[ "$OS" == "wsl2" || "$OS" == "linux" ]]; then if systemctl is-active ssh &>/dev/null 2>&1 || systemctl is-active sshd &>/dev/null 2>&1; then warn "SSH service is RUNNING — disable if not needed" \ "Disable SSH: sudo systemctl disable --now ssh" else pass "SSH service: not running" fi fi # Check UFW (Linux/WSL2) if [[ "$OS" == "wsl2" || "$OS" == "linux" ]]; then if command -v ufw &>/dev/null; then UFW_STATUS=$(sudo ufw status 2>/dev/null | head -1 || echo "unknown") if echo "$UFW_STATUS" | grep -q "active"; then pass "UFW firewall: active" else warn "UFW firewall: inactive" \ "Enable: sudo ufw default deny incoming && sudo ufw allow from 127.0.0.1 to any port $GW_PORT && sudo ufw enable" fi else warn "UFW not installed" \ "Install: sudo apt install -y ufw && sudo ufw default deny incoming && sudo ufw enable" fi fi # Check macOS firewall if [[ "$OS" == "macos" ]]; then FW_STATE=$(sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null | grep -o "enabled\|disabled" || echo "unknown") if [[ "$FW_STATE" == "enabled" ]]; then pass "macOS firewall: enabled" elif [[ "$FW_STATE" == "disabled" ]]; then warn "macOS firewall: disabled" \ "Enable: System Settings → Network → Firewall → Turn On" else info "Could not check macOS firewall state" fi fi # ============================================================================= section "5. API Key Security" # ============================================================================= if [[ -f "$CONFIG_FILE" ]]; then # Check for hardcoded API keys if grep -qE 'sk-[a-zA-Z0-9]{20,}' "$CONFIG_FILE" 2>/dev/null; then warn "Hardcoded OpenAI API key found in config.yaml" \ "Prefer OAuth login: openclaw auth login openai (keys in config are readable by anyone with file access)" else pass "No hardcoded OpenAI API key in config.yaml" fi if grep -qE 'sk-ant-[a-zA-Z0-9]{20,}' "$CONFIG_FILE" 2>/dev/null; then warn "Hardcoded Anthropic API key found in config.yaml" \ "Prefer OAuth login: openclaw auth login anthropic" else pass "No hardcoded Anthropic API key in config.yaml" fi # Check if config is in a git repo if git -C "$OPENCLAW_DIR" rev-parse --is-inside-work-tree &>/dev/null; then fail "~/.openclaw/ is inside a git repo — API keys may be committed!" \ "Add .openclaw/ to .gitignore immediately: echo '.openclaw/' >> ~/.gitignore_global" else pass "~/.openclaw/ is NOT inside a git repo" fi fi # ============================================================================= section "6. System Security" # ============================================================================= # Check OS updates if [[ "$OS" == "wsl2" || "$OS" == "linux" ]]; then UPGRADABLE=$(apt list --upgradable 2>/dev/null | grep -c "upgradable" || echo "0") if [[ "$UPGRADABLE" -gt 10 ]]; then warn "$UPGRADABLE packages have available updates" \ "Update: sudo apt update && sudo apt upgrade -y" elif [[ "$UPGRADABLE" -gt 0 ]]; then info "$UPGRADABLE packages have available updates" else pass "System packages: up to date" fi elif [[ "$OS" == "macos" ]]; then BREW_OUTDATED=$(brew outdated 2>/dev/null | wc -l | tr -d ' ') if [[ "$BREW_OUTDATED" -gt 10 ]]; then warn "$BREW_OUTDATED Homebrew packages are outdated" \ "Update: brew update && brew upgrade" else pass "Homebrew packages: mostly up to date ($BREW_OUTDATED outdated)" fi fi # Check WSL2 systemd if [[ "$OS" == "wsl2" ]]; then if [[ -f /etc/wsl.conf ]] && grep -q "systemd=true" /etc/wsl.conf 2>/dev/null; then pass "WSL2 systemd: enabled (daemon auto-start works)" else warn "WSL2 systemd not enabled — OpenClaw daemon won't auto-start" \ "Add [boot] systemd=true to /etc/wsl.conf and restart WSL (wsl --shutdown)" fi fi # Check for open ports if command -v ss &>/dev/null; then OPEN_PORTS=$(ss -tlnp 2>/dev/null | grep -c "0.0.0.0\|\[::\]" || echo "0") if [[ "$OPEN_PORTS" -gt 5 ]]; then warn "$OPEN_PORTS services listening on all interfaces" \ "Review open ports: ss -tlnp | grep '0.0.0.0' — close anything you don't need" else pass "Open ports on all interfaces: $OPEN_PORTS (reasonable)" fi elif command -v lsof &>/dev/null; then OPEN_PORTS=$(lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -c "\*:" || echo "0") if [[ "$OPEN_PORTS" -gt 5 ]]; then warn "$OPEN_PORTS services listening on all interfaces" \ "Review open ports: lsof -iTCP -sTCP:LISTEN -P -n | grep '\\*:' — close anything you don't need" else pass "Open ports on all interfaces: $OPEN_PORTS (reasonable)" fi fi # ============================================================================= section "7. OpenClaw Doctor" # ============================================================================= if command -v openclaw &>/dev/null; then info "Running 'openclaw doctor' for built-in health check..." echo "" openclaw doctor 2>&1 | sed 's/^/ /' || warn "openclaw doctor failed" "Run manually: openclaw doctor" echo "" else info "Skipping — OpenClaw not installed" fi # ============================================================================= # SUMMARY # ============================================================================= echo "" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BOLD} SECURITY SCAN SUMMARY${NC}" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" TOTAL=$((PASS + FAIL + WARN)) echo -e " ${GREEN}✅ Passed: $PASS${NC}" echo -e " ${RED}❌ Failed: $FAIL${NC}" echo -e " ${YELLOW}⚠️ Warnings: $WARN${NC}" echo -e " ─────────────────" echo -e " ${BOLD}Total checks: $TOTAL${NC}" echo "" if [[ $FAIL -eq 0 && $WARN -eq 0 ]]; then echo -e " ${GREEN}${BOLD}🎉 ALL CLEAR — Your OpenClaw setup is secure!${NC}" elif [[ $FAIL -eq 0 ]]; then echo -e " ${YELLOW}${BOLD}⚠️ MOSTLY SECURE — Review warnings below${NC}" elif [[ $FAIL -le 2 ]]; then echo -e " ${RED}${BOLD}🔴 ACTION REQUIRED — Fix the issues below before going live${NC}" else echo -e " ${RED}${BOLD}🚨 CRITICAL — Multiple security issues detected!${NC}" fi # Print recommendations if [[ ${#RECOMMENDATIONS[@]} -gt 0 ]]; then echo "" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BOLD} RECOMMENDATIONS (fix in order)${NC}" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" REC_NUM=1 for rec in "${RECOMMENDATIONS[@]}"; do echo -e " ${BOLD}${REC_NUM}.${NC} $rec" echo "" ((REC_NUM++)) done fi echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e " Run this script again after fixing issues: ${BOLD}bash validate-security.sh${NC}" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # Exit with non-zero if any failures if [[ $FAIL -gt 0 ]]; then exit 1 fi exit 0