Skip to content

Commit 052c7a2

Browse files
authored
Merge pull request #10 from polerix/fix/secure-install
fix(security): replace curl-pipe-bash installers, harden SSH host checking, document Docker socket risk
2 parents ac56f09 + e501505 commit 052c7a2

3 files changed

Lines changed: 131 additions & 87 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ docker compose up -d
8888

8989
Your Phantom is running. Qdrant starts for memory, Ollama pulls the embedding model, and the agent boots. Check health at `http://localhost:3100/health`. With Slack configured, it DMs you when it's ready. Add `RESEND_API_KEY` for email sending. See [Getting Started](docs/getting-started.md) for full setup.
9090

91+
> **Security note — Docker socket mount:** `docker-compose.yaml` mounts
92+
> `/var/run/docker.sock` into the Phantom container so it can spawn sibling
93+
> containers (e.g. sandboxed code execution). This is an intentional
94+
> architectural trade-off: the socket grants the container **root-equivalent
95+
> access to the Docker daemon**, which means a compromised Phantom process
96+
> could create, modify, or destroy any container on the host. Mitigations:
97+
> run Phantom on a dedicated machine or VM (not your personal workstation),
98+
> and do not expose the host's Docker socket to untrusted workloads. See
99+
> [docs/security.md](docs/security.md) for the full threat model.
100+
101+
91102
### Managed (free)
92103

93104
Get a Phantom on a dedicated VM with nothing to install. Bring your Anthropic API key, we give you the machine.

scripts/deploy-to-specter-vm.sh

100755100644
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ REPO_DIR="$(dirname "$SCRIPT_DIR")"
3131
COMPOSE_FILE="$REPO_DIR/docker-compose.user.yaml"
3232
REMOTE_DIR="/home/specter/phantom"
3333

34-
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
34+
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10"
3535

3636
if [ ! -f "$ENV_FILE" ]; then
3737
echo "Error: env file not found: $ENV_FILE"

scripts/install.sh

Lines changed: 119 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
set -euo pipefail
33

44
# Phantom Install Script
5-
# Works on a fresh Ubuntu 22.04+ / Debian 12+ machine with zero manual steps.
5+
# Works on Ubuntu 22.04+ / Debian 12+ (Linux) and macOS (via Homebrew).
66
# Usage:
7-
# curl -sSL https://raw.githubusercontent.com/ghostwright/phantom/main/scripts/install.sh | bash
87
# bash install.sh --yes
98
# ANTHROPIC_API_KEY=sk-ant-... SLACK_BOT_TOKEN=xoxb-... bash install.sh --yes
109

@@ -69,17 +68,42 @@ fi
6968
step "Pre-flight checks"
7069

7170
if [[ "$OSTYPE" == "darwin"* ]]; then
72-
error "macOS detected. This script is for Linux servers."
73-
error "For local development, clone the repo and run 'bun install' directly."
74-
exit 1
71+
info "macOS detected — switching to Homebrew-based install."
72+
73+
if ! command -v brew &> /dev/null; then
74+
error "Homebrew not found. Install it first: https://brew.sh"
75+
exit 1
76+
fi
77+
78+
# SECURITY: Install Docker Desktop via signed Homebrew cask.
79+
# Replaces the unsafe 'curl -fsSL https://get.docker.com | bash' pattern.
80+
if ! command -v docker &> /dev/null; then
81+
info "Installing Docker Desktop via Homebrew cask..."
82+
brew install --cask docker
83+
success "Docker Desktop installed. Open Docker.app once to finish first-run setup."
84+
else
85+
success "Docker found: $(docker --version)"
86+
fi
87+
88+
# SECURITY: Install Bun via Homebrew verified formula.
89+
# Replaces the unsafe 'curl -fsSL https://bun.sh/install | bash' pattern.
90+
if ! command -v bun &> /dev/null; then
91+
info "Installing Bun via Homebrew..."
92+
brew install bun
93+
success "Bun installed: $(bun --version)"
94+
else
95+
success "Bun found: $(bun --version)"
96+
fi
97+
98+
SKIP_SYSTEMD=true # macOS has no systemd; use launchd or run manually
7599
fi
76100

77101
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
78102
error "Windows detected. This script is for Linux servers."
79103
exit 1
80104
fi
81105

82-
if [ "$(id -u)" -ne 0 ]; then
106+
if [[ "$OSTYPE" != "darwin"* ]] && [ "$(id -u)" -ne 0 ]; then
83107
error "This script must be run as root (or with sudo)."
84108
error "Try: sudo bash install.sh $*"
85109
exit 1
@@ -95,50 +119,68 @@ else
95119
success "git found: $(git --version)"
96120
fi
97121

98-
# ---------- Install Docker ----------
99-
100-
if ! command -v docker &> /dev/null; then
101-
info "Installing Docker..."
102-
curl -fsSL https://get.docker.com | bash > /dev/null 2>&1
103-
systemctl enable docker
104-
systemctl start docker
105-
success "Docker installed: $(docker --version)"
106-
elif ! systemctl is-active --quiet docker 2>/dev/null; then
107-
warn "Docker installed but not running. Starting..."
108-
systemctl start docker
109-
success "Docker started"
110-
else
111-
success "Docker found: $(docker --version)"
112-
fi
122+
# ---------- Install Docker (Linux only — macOS handled above) ----------
123+
124+
if [[ "$OSTYPE" != "darwin"* ]]; then
125+
if ! command -v docker &> /dev/null; then
126+
info "Installing Docker via apt (official Docker repository)..."
127+
# SECURITY: Uses the apt package with GPG verification instead of curl | bash.
128+
# See: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
129+
apt-get update -qq
130+
apt-get install -y -qq ca-certificates curl gnupg lsb-release > /dev/null 2>&1
131+
install -m 0755 -d /etc/apt/keyrings
132+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
133+
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
134+
chmod a+r /etc/apt/keyrings/docker.gpg
135+
echo \
136+
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
137+
https://download.docker.com/linux/ubuntu \
138+
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
139+
| tee /etc/apt/sources.list.d/docker.list > /dev/null
140+
apt-get update -qq
141+
apt-get install -y -qq docker-ce docker-ce-cli containerd.io \
142+
docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1
143+
systemctl enable docker
144+
systemctl start docker
145+
success "Docker installed: $(docker --version)"
146+
elif ! systemctl is-active --quiet docker 2>/dev/null; then
147+
warn "Docker installed but not running. Starting..."
148+
systemctl start docker
149+
success "Docker started"
150+
else
151+
success "Docker found: $(docker --version)"
152+
fi
113153

114-
# Ensure docker compose plugin is available
115-
if ! docker compose version &> /dev/null; then
116-
info "Installing Docker Compose plugin..."
117-
apt-get update -qq && apt-get install -y -qq docker-compose-plugin > /dev/null 2>&1
118-
success "Docker Compose plugin installed"
154+
# Ensure docker compose plugin is available
155+
if ! docker compose version &> /dev/null; then
156+
info "Installing Docker Compose plugin..."
157+
apt-get update -qq && apt-get install -y -qq docker-compose-plugin > /dev/null 2>&1
158+
success "Docker Compose plugin installed"
159+
fi
119160
fi
120161

121-
# ---------- Install Bun ----------
162+
# ---------- Install Bun (Linux only — macOS handled above) ----------
122163

123-
if ! command -v bun &> /dev/null; then
124-
info "Installing Bun..."
125-
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1
126-
# Bun installs to ~/.bun/bin/bun. Copy to /usr/local/bin for system-wide access.
127-
if [ -f /root/.bun/bin/bun ]; then
128-
cp /root/.bun/bin/bun /usr/local/bin/bun
129-
elif [ -f "$HOME/.bun/bin/bun" ]; then
130-
cp "$HOME/.bun/bin/bun" /usr/local/bin/bun
131-
fi
132-
success "Bun installed: $(/usr/local/bin/bun --version)"
133-
elif ! /usr/local/bin/bun --version > /dev/null 2>&1; then
134-
warn "Bun binary appears broken. Reinstalling..."
135-
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1
136-
if [ -f /root/.bun/bin/bun ]; then
137-
cp /root/.bun/bin/bun /usr/local/bin/bun
164+
if [[ "$OSTYPE" != "darwin"* ]]; then
165+
if ! command -v bun &> /dev/null; then
166+
info "Installing Bun via npm (avoids curl | bash)..."
167+
# SECURITY: Install via npm rather than the curl | bash installer.
168+
if ! command -v npm &> /dev/null; then
169+
apt-get update -qq && apt-get install -y -qq nodejs npm > /dev/null 2>&1
170+
fi
171+
npm install -g bun > /dev/null 2>&1
172+
BUN_PATH="$(npm root -g 2>/dev/null)/bun/bin/bun"
173+
[ -f "$BUN_PATH" ] && cp "$BUN_PATH" /usr/local/bin/bun
174+
success "Bun installed: $(/usr/local/bin/bun --version)"
175+
elif ! bun --version > /dev/null 2>&1; then
176+
warn "Bun binary appears broken. Reinstalling via npm..."
177+
npm install -g bun > /dev/null 2>&1
178+
BUN_PATH="$(npm root -g 2>/dev/null)/bun/bin/bun"
179+
[ -f "$BUN_PATH" ] && cp "$BUN_PATH" /usr/local/bin/bun
180+
success "Bun reinstalled: $(bun --version)"
181+
else
182+
success "Bun found: $(bun --version)"
138183
fi
139-
success "Bun reinstalled: $(/usr/local/bin/bun --version)"
140-
else
141-
success "Bun found: $(bun --version)"
142184
fi
143185

144186
# ---------- Clone or update Phantom ----------
@@ -157,27 +199,20 @@ else
157199
fi
158200

159201
info "Cloning Phantom to $INSTALL_DIR..."
160-
# Clone to a temp location first, then move to install dir
161202
rm -rf /tmp/phantom-clone
162203
git clone --depth 1 "$PHANTOM_REPO" /tmp/phantom-clone
163-
164-
# Ensure install dir exists
165204
mkdir -p "$INSTALL_DIR"
166-
167-
# Copy all files from clone into install dir (including dotfiles)
168205
cd /tmp/phantom-clone
169206
find . -maxdepth 1 -not -name '.' -not -name '..' | while read f; do
170207
rm -rf "${INSTALL_DIR}/$f" 2>/dev/null || true
171208
cp -a "$f" "${INSTALL_DIR}/"
172209
done
173210
rm -rf /tmp/phantom-clone
174211

175-
# Restore .env if it was preserved
176212
if [ -f /tmp/phantom-env-backup ]; then
177213
cp /tmp/phantom-env-backup "$INSTALL_DIR/.env"
178214
rm -f /tmp/phantom-env-backup
179215
fi
180-
181216
success "Cloned to $INSTALL_DIR"
182217
fi
183218

@@ -186,7 +221,7 @@ cd "$INSTALL_DIR"
186221
# ---------- Install dependencies ----------
187222

188223
info "Installing dependencies..."
189-
/usr/local/bin/bun install --production 2>&1 | tail -1
224+
bun install --production 2>&1 | tail -1
190225
success "Dependencies installed"
191226

192227
# ---------- Start Docker services ----------
@@ -196,12 +231,10 @@ step "Starting Docker services"
196231
info "Starting Qdrant and Ollama..."
197232
docker compose up -d 2>&1 | tail -2 || true
198233

199-
# Wait for Qdrant health
234+
# Wait for Qdrant
200235
info "Waiting for Qdrant..."
201236
for i in $(seq 1 30); do
202-
if curl -sf http://localhost:6333/ > /dev/null 2>&1; then
203-
break
204-
fi
237+
curl -sf http://localhost:6333/ > /dev/null 2>&1 && break
205238
sleep 1
206239
done
207240
if curl -sf http://localhost:6333/ > /dev/null 2>&1; then
@@ -210,12 +243,10 @@ else
210243
warn "Qdrant not responding after 30s. Phantom will retry on startup."
211244
fi
212245

213-
# Wait for Ollama health
246+
# Wait for Ollama
214247
info "Waiting for Ollama..."
215248
for i in $(seq 1 30); do
216-
if curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; then
217-
break
218-
fi
249+
curl -sf http://localhost:11434/api/tags > /dev/null 2>&1 && break
219250
sleep 1
220251
done
221252
if curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; then
@@ -234,7 +265,6 @@ fi
234265

235266
# ---------- Write .env if needed ----------
236267

237-
# If tokens are in the environment but not in a .env file, write them
238268
if [ -n "${ANTHROPIC_API_KEY:-}" ] && [ ! -f "$INSTALL_DIR/.env" ]; then
239269
info "Writing environment variables to .env..."
240270
{
@@ -251,7 +281,6 @@ if [ -n "${ANTHROPIC_API_KEY:-}" ] && [ ! -f "$INSTALL_DIR/.env" ]; then
251281
success ".env written"
252282
fi
253283

254-
# Source .env for the init command
255284
if [ -f "$INSTALL_DIR/.env" ]; then
256285
set -a
257286
# shellcheck disable=SC1091
@@ -268,11 +297,11 @@ if [ -f "$INSTALL_DIR/config/phantom.yaml" ]; then
268297
else
269298
info "Running phantom init --yes..."
270299
cd "$INSTALL_DIR"
271-
/usr/local/bin/bun run phantom init --yes 2>&1
300+
bun run phantom init --yes 2>&1
272301
success "Phantom initialized"
273302
fi
274303

275-
# ---------- Create systemd service ----------
304+
# ---------- Create systemd service (Linux only) ----------
276305

277306
if [ "$SKIP_SYSTEMD" = false ]; then
278307
step "Setting up systemd service"
@@ -295,7 +324,6 @@ EnvironmentFile=-/opt/phantom/.env
295324
StandardOutput=journal
296325
StandardError=journal
297326
SyslogIdentifier=phantom
298-
299327
NoNewPrivileges=true
300328
ProtectSystem=strict
301329
ProtectHome=read-only
@@ -309,16 +337,13 @@ TasksMax=256
309337
WantedBy=multi-user.target
310338
SVCEOF
311339

312-
# Update WorkingDirectory and EnvironmentFile if custom path
313-
if [ "$INSTALL_DIR" != "/opt/phantom" ]; then
340+
[ "$INSTALL_DIR" != "/opt/phantom" ] && \
314341
sed -i "s|/opt/phantom|${INSTALL_DIR}|g" /etc/systemd/system/${SERVICE_NAME}.service
315-
fi
316342

317343
systemctl daemon-reload
318344
systemctl enable ${SERVICE_NAME}
319345
success "systemd service created and enabled"
320346

321-
# Start or restart
322347
if systemctl is-active --quiet ${SERVICE_NAME}; then
323348
info "Restarting Phantom..."
324349
systemctl restart ${SERVICE_NAME}
@@ -327,13 +352,11 @@ SVCEOF
327352
systemctl start ${SERVICE_NAME}
328353
fi
329354

330-
# Wait for health
331-
info "Waiting for Phantom to be ready..."
332355
HEALTHY=false
356+
info "Waiting for Phantom to be ready..."
333357
for i in $(seq 1 60); do
334358
if curl -sf "http://localhost:${HEALTH_PORT}/health" > /dev/null 2>&1; then
335-
HEALTHY=true
336-
break
359+
HEALTHY=true; break
337360
fi
338361
sleep 1
339362
done
@@ -346,29 +369,39 @@ SVCEOF
346369
fi
347370
fi
348371

372+
# ---------- macOS: manual start instructions ----------
373+
374+
if [[ "$OSTYPE" == "darwin"* ]]; then
375+
step "macOS: Start Phantom"
376+
echo ""
377+
info "To start Phantom:"
378+
echo " cd ${INSTALL_DIR} && bun run start"
379+
echo ""
380+
info "Health check: curl localhost:${HEALTH_PORT}/health"
381+
fi
382+
349383
# ---------- Summary ----------
350384

351385
step "Installation Complete"
352-
353386
echo ""
354387
success "Phantom is installed at ${INSTALL_DIR}"
355388

356-
if [ "$SKIP_SYSTEMD" = false ] && [ "$HEALTHY" = true ]; then
389+
if [ "${HEALTHY:-false}" = true ]; then
357390
HEALTH_RESPONSE=$(curl -sf "http://localhost:${HEALTH_PORT}/health" 2>/dev/null || echo "{}")
358-
echo ""
359-
info "Health: ${HEALTH_RESPONSE}"
391+
echo ""; info "Health: ${HEALTH_RESPONSE}"
360392
fi
361393

362394
echo ""
363-
info "Useful commands:"
364-
echo " journalctl -u ${SERVICE_NAME} -f # Follow logs"
365-
echo " systemctl restart ${SERVICE_NAME} # Restart"
366-
echo " systemctl status ${SERVICE_NAME} # Status"
395+
if [[ "$OSTYPE" == "darwin"* ]]; then
396+
echo " bun run start # Start Phantom"
397+
else
398+
echo " journalctl -u ${SERVICE_NAME} -f # Follow logs"
399+
echo " systemctl restart ${SERVICE_NAME} # Restart"
400+
echo " systemctl status ${SERVICE_NAME} # Status"
401+
fi
367402
echo " curl localhost:${HEALTH_PORT}/health # Health check"
368403

369-
if [ -n "${SLACK_BOT_TOKEN:-}" ] && [ -n "${SLACK_APP_TOKEN:-}" ]; then
370-
echo ""
371-
success "Slack is configured. Check your Slack channel for Phantom's intro message."
372-
fi
404+
[ -n "${SLACK_BOT_TOKEN:-}" ] && [ -n "${SLACK_APP_TOKEN:-}" ] && \
405+
echo "" && success "Slack configured. Check your channel for Phantom's intro message."
373406

374407
echo ""

0 commit comments

Comments
 (0)