Skip to content

Commit 6f814e5

Browse files
Security hardening and installer resilience audit (#72)
- Prevent .env overwrite on re-install: _env_get() merges existing user secrets/API keys instead of regenerating them - Add ERR trap with phase tracking to install-core.sh for actionable failure diagnostics - Replace unsafe `source .env` with line-by-line parser in dream-preflight.sh - Fix sed injection in 4 files (dashboard entrypoint, 06-directories, 07-devtools) by escaping special characters before substitution - Fix JSON injection in dream-backup.sh and dream-update.sh by using jq --arg instead of heredoc interpolation - Add tar path traversal protection in dream-restore.sh - Fix curl auth header quoting in dream-update.sh (array instead of string) - Add jq prerequisite check to dream-backup.sh and dream-update.sh - Add model download retry (3 attempts with resume) in 11-services.sh - Fix `local` keyword used outside function in 11-services.sh - Track health check failures with counter instead of swallowing via || true - Add port check fallback warning when ss/netstat missing - Remove duplicate LLAMA_SERVER_PORT from generated .env - Preserve LIVEKIT_API_KEY on re-install via _env_get - Add dream-uninstall.sh for clean removal - Add archive/ to .gitignore Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a0ac2a6 commit 6f814e5

13 files changed

Lines changed: 417 additions & 99 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ dream-server/.claude/
6060
dream-server/.pytest_cache/
6161
dream-server/tests/__pycache__/
6262
dream-server/internal/
63+
64+
# ============================================
65+
# Archived / deprecated code (pre-v2.0)
66+
# ============================================
67+
archive/

dream-server/dream-backup.sh

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ NC='\033[0m' # No Color
2020

2121
# Prerequisites check
2222
command -v rsync >/dev/null 2>&1 || { echo -e "${RED}Error: rsync is required but not installed.${NC}" >&2; echo "Install with: apt install rsync (Debian/Ubuntu) or brew install rsync (macOS)" >&2; exit 1; }
23+
command -v jq >/dev/null 2>&1 || { echo -e "${RED}Error: jq is required but not installed.${NC}" >&2; echo "Install with: apt install jq (Debian/Ubuntu) or brew install jq (macOS)" >&2; exit 1; }
2324

2425
# Logging functions
2526
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
@@ -126,31 +127,42 @@ create_manifest() {
126127
local version
127128
version=$(cat "$DREAM_DIR/.version" 2>/dev/null || echo "unknown")
128129

129-
cat > "$backup_dir/manifest.json" << EOF
130-
{
131-
"manifest_version": "1.0",
132-
"backup_date": "$(date -Iseconds)",
133-
"backup_id": "$(basename "$backup_dir")",
134-
"backup_type": "$backup_type",
135-
"dream_version": "$version",
136-
"hostname": "$(hostname)",
137-
"description": "$description",
138-
"contents": {
139-
"user_data": $( [[ "$backup_type" == "full" || "$backup_type" == "user-data" ]] && echo "true" || echo "false" ),
140-
"config": $( [[ "$backup_type" == "full" || "$backup_type" == "config" ]] && echo "true" || echo "false" ),
141-
"cache": $( [[ "$backup_type" == "full" ]] && echo "true" || echo "false" )
142-
},
143-
"paths": {
144-
"data_open_webui": "data/open-webui",
145-
"data_n8n": "data/n8n",
146-
"data_qdrant": "data/qdrant",
147-
"data_openclaw": "data/openclaw",
148-
"env": ".env",
149-
"compose": "docker-compose.yml",
150-
"config": "config"
151-
}
152-
}
153-
EOF
130+
# Use jq to safely construct JSON (prevents injection via $description)
131+
local has_user_data="false" has_config="false" has_cache="false"
132+
[[ "$backup_type" == "full" || "$backup_type" == "user-data" ]] && has_user_data="true"
133+
[[ "$backup_type" == "full" || "$backup_type" == "config" ]] && has_config="true"
134+
[[ "$backup_type" == "full" ]] && has_cache="true"
135+
136+
jq -n \
137+
--arg mv "1.0" \
138+
--arg bd "$(date -Iseconds)" \
139+
--arg bi "$(basename "$backup_dir")" \
140+
--arg bt "$backup_type" \
141+
--arg dv "$version" \
142+
--arg hn "$(hostname)" \
143+
--arg desc "$description" \
144+
--argjson ud "$has_user_data" \
145+
--argjson cfg "$has_config" \
146+
--argjson ca "$has_cache" \
147+
'{
148+
manifest_version: $mv,
149+
backup_date: $bd,
150+
backup_id: $bi,
151+
backup_type: $bt,
152+
dream_version: $dv,
153+
hostname: $hn,
154+
description: $desc,
155+
contents: { user_data: $ud, config: $cfg, cache: $ca },
156+
paths: {
157+
data_open_webui: "data/open-webui",
158+
data_n8n: "data/n8n",
159+
data_qdrant: "data/qdrant",
160+
data_openclaw: "data/openclaw",
161+
env: ".env",
162+
compose: "docker-compose.yml",
163+
config: "config"
164+
}
165+
}' > "$backup_dir/manifest.json"
154166
log_info "Created backup manifest"
155167
}
156168

dream-server/dream-preflight.sh

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1010
DREAM_DIR="$SCRIPT_DIR"
1111
LOG_FILE="$DREAM_DIR/preflight-$(date +%Y%m%d-%H%M%S).log"
1212

13-
# Load config from .env if available
13+
# Load config from .env safely (line-by-line, no eval/source)
1414
if [ -f "$DREAM_DIR/.env" ]; then
15-
# shellcheck source=/dev/null
16-
source "$DREAM_DIR/.env" 2>/dev/null || true
15+
while IFS='=' read -r key value; do
16+
# Skip comments and empty lines
17+
[[ "$key" =~ ^[[:space:]]*# ]] && continue
18+
[[ -z "$key" ]] && continue
19+
# Only allow safe variable names
20+
key=$(echo "$key" | xargs) # trim whitespace
21+
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
22+
# Strip surrounding quotes from value
23+
value="${value%\"}" && value="${value#\"}"
24+
value="${value%\'}" && value="${value#\'}"
25+
export "$key=$value"
26+
done < "$DREAM_DIR/.env"
1727
fi
1828
SERVICE_HOST="${SERVICE_HOST:-localhost}"
1929

dream-server/dream-restore.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,18 @@ extract_backup() {
150150
fi
151151

152152
if [[ -f "$compressed" ]]; then
153+
# Validate: reject archives with absolute paths or path traversal
154+
if tar -tzf "$compressed" 2>/dev/null | grep -qE '(^/|\.\./)'; then
155+
log_error "Backup archive contains unsafe paths (absolute or ../) — refusing to extract"
156+
return 1
157+
fi
153158
log_info "Extracting compressed backup..."
154159
mkdir -p "$uncompressed"
155-
tar xzf "$compressed" -C "$BACKUP_ROOT"
160+
if ! tar xzf "$compressed" --no-same-owner -C "$BACKUP_ROOT"; then
161+
log_error "Failed to extract backup archive"
162+
rm -rf "$uncompressed"
163+
return 1
164+
fi
156165
echo "$uncompressed"
157166
return 0
158167
fi

dream-server/dream-uninstall.sh

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/bin/bash
2+
# dream-uninstall.sh - Dream Server Clean Uninstaller
3+
# Removes all Dream Server components, data, and system modifications.
4+
# Usage: ./dream-uninstall.sh [--keep-models] [--keep-data] [--force]
5+
6+
set -euo pipefail
7+
8+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
INSTALL_DIR="${INSTALL_DIR:-$HOME/dream-server}"
10+
11+
# Colors
12+
RED='\033[0;31m'
13+
GREEN='\033[0;32m'
14+
YELLOW='\033[1;33m'
15+
BLUE='\033[0;34m'
16+
NC='\033[0m'
17+
18+
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
19+
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
20+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
21+
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
22+
23+
KEEP_MODELS=false
24+
KEEP_DATA=false
25+
FORCE=false
26+
27+
while [[ $# -gt 0 ]]; do
28+
case "$1" in
29+
--keep-models) KEEP_MODELS=true; shift ;;
30+
--keep-data) KEEP_DATA=true; shift ;;
31+
--force) FORCE=true; shift ;;
32+
-h|--help)
33+
cat << EOF
34+
Dream Server Uninstaller
35+
36+
Usage: $(basename "$0") [OPTIONS]
37+
38+
Options:
39+
--keep-models Keep downloaded AI models (saves re-download time)
40+
--keep-data Keep user data (chat history, n8n workflows, etc.)
41+
--force Skip confirmation prompts
42+
-h, --help Show this help
43+
44+
This will remove:
45+
- Docker containers, images, and volumes for Dream Server
46+
- Installation directory ($INSTALL_DIR)
47+
- Systemd user services (opencode-web, openclaw timers)
48+
- CLI symlink (/usr/local/bin/dream-cli)
49+
- Backup directory (~/.dream-server)
50+
51+
EOF
52+
exit 0
53+
;;
54+
*) log_error "Unknown option: $1"; exit 1 ;;
55+
esac
56+
done
57+
58+
echo ""
59+
echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"
60+
echo -e "${RED}║ DREAM SERVER UNINSTALLER ║${NC}"
61+
echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"
62+
echo ""
63+
64+
# Detect install dir
65+
if [[ -d "$SCRIPT_DIR" && -f "$SCRIPT_DIR/dream-cli" ]]; then
66+
INSTALL_DIR="$SCRIPT_DIR"
67+
fi
68+
69+
if [[ ! -d "$INSTALL_DIR" ]]; then
70+
log_error "Install directory not found: $INSTALL_DIR"
71+
exit 1
72+
fi
73+
74+
log_info "Install directory: $INSTALL_DIR"
75+
$KEEP_MODELS && log_info "Keeping models (--keep-models)"
76+
$KEEP_DATA && log_info "Keeping user data (--keep-data)"
77+
echo ""
78+
79+
if [[ "$FORCE" != "true" ]]; then
80+
echo -e "${YELLOW}This will permanently remove Dream Server and its components.${NC}"
81+
read -rp "Are you sure? Type 'yes' to confirm: " confirm
82+
if [[ "$confirm" != "yes" ]]; then
83+
log_info "Uninstall cancelled."
84+
exit 0
85+
fi
86+
echo ""
87+
fi
88+
89+
# 1. Stop and remove Docker containers
90+
log_info "Stopping Docker containers..."
91+
cd "$INSTALL_DIR" 2>/dev/null || true
92+
if command -v docker &>/dev/null; then
93+
# Try docker compose first, fall back to finding dream containers
94+
docker compose down --remove-orphans 2>/dev/null || true
95+
96+
# Remove any remaining dream-* containers
97+
dream_containers=$(docker ps -a --filter "name=dream-" --format "{{.Names}}" 2>/dev/null || true)
98+
if [[ -n "$dream_containers" ]]; then
99+
log_info "Removing Dream Server containers..."
100+
echo "$dream_containers" | xargs -r docker rm -f 2>/dev/null || true
101+
fi
102+
103+
# Remove dream-specific Docker volumes
104+
dream_volumes=$(docker volume ls --filter "name=dream" --format "{{.Name}}" 2>/dev/null || true)
105+
if [[ -n "$dream_volumes" ]]; then
106+
log_info "Removing Docker volumes..."
107+
echo "$dream_volumes" | xargs -r docker volume rm 2>/dev/null || true
108+
fi
109+
110+
log_ok "Docker cleanup complete"
111+
else
112+
log_warn "Docker not found — skipping container cleanup"
113+
fi
114+
115+
# 2. Stop and remove systemd user services
116+
log_info "Removing systemd user services..."
117+
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
118+
for unit in opencode-web.service openclaw-session-cleanup.timer openclaw-session-manager.timer \
119+
memory-shepherd-workspace.timer memory-shepherd-memory.timer \
120+
openclaw-session-cleanup.service openclaw-session-manager.service \
121+
memory-shepherd-workspace.service memory-shepherd-memory.service; do
122+
if [[ -f "$SYSTEMD_USER_DIR/$unit" ]]; then
123+
systemctl --user disable --now "$unit" 2>/dev/null || true
124+
rm -f "$SYSTEMD_USER_DIR/$unit"
125+
fi
126+
done
127+
systemctl --user daemon-reload 2>/dev/null || true
128+
log_ok "Systemd services removed"
129+
130+
# 3. Remove CLI symlink
131+
if [[ -L "/usr/local/bin/dream-cli" ]]; then
132+
log_info "Removing CLI symlink..."
133+
sudo rm -f /usr/local/bin/dream-cli 2>/dev/null || rm -f /usr/local/bin/dream-cli 2>/dev/null || true
134+
log_ok "CLI symlink removed"
135+
fi
136+
137+
# 4. Remove desktop file
138+
DESKTOP_FILE="$HOME/.local/share/applications/dream-server.desktop"
139+
if [[ -f "$DESKTOP_FILE" ]]; then
140+
rm -f "$DESKTOP_FILE"
141+
log_ok "Desktop entry removed"
142+
fi
143+
144+
# 5. Remove install directory (with optional data/model preservation)
145+
log_info "Removing installation directory..."
146+
if $KEEP_MODELS && [[ -d "$INSTALL_DIR/data/models" ]]; then
147+
MODELS_BACKUP="$HOME/.dream-server-models-backup"
148+
mkdir -p "$MODELS_BACKUP"
149+
mv "$INSTALL_DIR/data/models"/* "$MODELS_BACKUP/" 2>/dev/null || true
150+
log_info "Models preserved at: $MODELS_BACKUP"
151+
fi
152+
153+
if $KEEP_DATA; then
154+
# Remove everything except data/
155+
find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 ! -name 'data' -exec rm -rf {} + 2>/dev/null || true
156+
log_info "User data preserved at: $INSTALL_DIR/data/"
157+
else
158+
rm -rf "$INSTALL_DIR"
159+
fi
160+
log_ok "Installation directory cleaned"
161+
162+
# 6. Remove backup directory
163+
if [[ -d "$HOME/.dream-server" ]]; then
164+
log_info "Removing backup directory..."
165+
rm -rf "$HOME/.dream-server"
166+
log_ok "Backups removed"
167+
fi
168+
169+
# 7. Remove OpenCode config (if we created it)
170+
OPENCODE_CONFIG="$HOME/.config/opencode/opencode.json"
171+
if [[ -f "$OPENCODE_CONFIG" ]] && grep -q "llama-server" "$OPENCODE_CONFIG" 2>/dev/null; then
172+
rm -f "$OPENCODE_CONFIG"
173+
log_ok "OpenCode config removed"
174+
fi
175+
176+
echo ""
177+
echo -e "${GREEN}╔══════════════════════════════════════════════════╗${NC}"
178+
echo -e "${GREEN}║ Dream Server has been uninstalled. ║${NC}"
179+
echo -e "${GREEN}╚══════════════════════════════════════════════════╝${NC}"
180+
echo ""
181+
if $KEEP_MODELS; then
182+
echo "Your models were saved to: $HOME/.dream-server-models-backup"
183+
echo "To reuse them on reinstall, move them back to ~/dream-server/data/models/"
184+
fi
185+
if $KEEP_DATA; then
186+
echo "Your user data was preserved at: $INSTALL_DIR/data/"
187+
fi

dream-server/dream-update.sh

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
set -euo pipefail
1414

15+
# Prerequisites
16+
command -v jq >/dev/null 2>&1 || { echo "Error: jq is required but not installed." >&2; echo "Install with: apt install jq (Debian/Ubuntu) or brew install jq (macOS)" >&2; exit 1; }
17+
1518
#==============================================================================
1619
# CONFIGURATION
1720
#==============================================================================
@@ -94,13 +97,13 @@ cmd_check() {
9497

9598
# Fetch latest release from GitHub
9699
local api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
97-
local auth_header=""
100+
local response
101+
local curl_args=(-sf)
98102
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
99-
auth_header="-H \"Authorization: Bearer ${GITHUB_TOKEN}\""
103+
curl_args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
100104
fi
101-
102-
local response
103-
if ! response=$(curl -sf ${auth_header} "${api_url}" 2>/dev/null); then
105+
106+
if ! response=$(curl "${curl_args[@]}" "${api_url}" 2>/dev/null); then
104107
log_error "Failed to check for updates. Check network or GITHUB_TOKEN."
105108
return 1
106109
fi
@@ -221,16 +224,15 @@ cmd_backup() {
221224
((files_backed_up++))
222225
fi
223226

224-
# Generate metadata
225-
cat > "$backup_path/metadata.json" << EOF
226-
{
227-
"backup_id": "${backup_id}",
228-
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
229-
"version": "$(get_current_version)",
230-
"files_count": ${files_backed_up},
231-
"install_dir": "${INSTALL_DIR}"
232-
}
233-
EOF
227+
# Generate metadata (use jq for safe JSON construction)
228+
jq -n \
229+
--arg bid "$backup_id" \
230+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
231+
--arg ver "$(get_current_version)" \
232+
--argjson fc "$files_backed_up" \
233+
--arg dir "$INSTALL_DIR" \
234+
'{backup_id: $bid, timestamp: $ts, version: $ver, files_count: $fc, install_dir: $dir}' \
235+
> "$backup_path/metadata.json"
234236

235237
log_ok "Backup created: ${backup_path}"
236238
log_info "Files backed up: ${files_backed_up}"

dream-server/extensions/services/dashboard/entrypoint.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ fi
2424
# We need to substitute it with the actual value
2525
if [ -n "$API_KEY" ]; then
2626
echo "[dashboard] Configuring nginx with API key auth injection"
27+
# Escape sed special characters in the API key to prevent injection
28+
ESCAPED_KEY=$(printf '%s\n' "$API_KEY" | sed 's/[&/\]/\\&/g')
2729
# Replace the placeholder (envsubst already ran, but may have left empty value)
28-
sed -i "s|Bearer \${DASHBOARD_API_KEY}|Bearer $API_KEY|g" "$NGINX_CONF"
29-
sed -i "s|Bearer \"\"|Bearer \"$API_KEY\"|g" "$NGINX_CONF"
30+
sed -i "s|Bearer \${DASHBOARD_API_KEY}|Bearer ${ESCAPED_KEY}|g" "$NGINX_CONF"
31+
sed -i "s|Bearer \"\"|Bearer \"${ESCAPED_KEY}\"|g" "$NGINX_CONF"
3032
else
3133
echo "[dashboard] WARNING: No DASHBOARD_API_KEY found in env or $KEY_FILE"
3234
echo "[dashboard] API calls will fail with 401 until key is configured"

0 commit comments

Comments
 (0)