-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·355 lines (312 loc) · 13.5 KB
/
Copy pathsetup.sh
File metadata and controls
executable file
·355 lines (312 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#!/usr/bin/env bash
# setup.sh - host bootstrap for the Ghost Agent Platform.
#
# Prompts for the release tag and Docker Hub OAT, generates the
# one-time claim token, fetches the stack bundle (compose file +
# static config defaults), and copies the defaults into place. All
# platform configuration - domain, TLS, admin account, connectors -
# happens afterwards in the in-product setup wizard, unlocked by the
# claim token this script prints.
#
# Refuses to overwrite existing config files. Remove them manually and
# re-run to regenerate.
set -euo pipefail
cd "$(dirname "$0")"
# ANSI color helpers - skip when not on a TTY so logs stay clean.
if [ -t 1 ]; then
B=$'\033[1m'; G=$'\033[0;32m'; Y=$'\033[1;33m'; R=$'\033[0;31m'; N=$'\033[0m'
else
B=""; G=""; Y=""; R=""; N=""
fi
# --- preflight ---
# The deploy directory is locked to /opt/exo. In-stack upgrades run
# `docker compose` from inside the updater container with the project
# directory fixed at /opt/exo, so the compose file's relative bind
# mounts (./config.toml, ./Caddyfile, …) resolve to /opt/exo/... on the
# host. Deploying elsewhere works for the first `docker compose up` but
# breaks the first upgrade (recreated services would bind nonexistent
# host paths). Fail fast here rather than at upgrade time.
if [ "$PWD" != "/opt/exo" ]; then
echo "${R}error:${N} this stack must be deployed at /opt/exo (current: ${PWD})."
echo " Move the repo to /opt/exo and re-run, e.g.:"
echo " sudo mv \"$PWD\" /opt/exo && cd /opt/exo && ./setup.sh"
exit 1
fi
# Don't overwrite an existing config.
for f in .env config.toml config.proxy.toml Caddyfile; do
if [ -f "$f" ]; then
echo "${R}error:${N} $f already exists. Remove it and re-run."
exit 1
fi
done
for tool in openssl curl; do
command -v "$tool" >/dev/null 2>&1 || { echo "${R}error:${N} '$tool' not found in PATH"; exit 1; }
done
# Docker (with the compose v2 plugin) is a hard requirement: the stack runs
# as `docker compose`, and the host-tuning below restarts docker.service.
# Fail fast with a clear message rather than aborting cryptically later.
if ! command -v docker >/dev/null 2>&1; then
echo "${R}error:${N} docker not found. Install Docker Engine + the compose plugin first:"
echo " https://docs.docker.com/engine/install/"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "${R}error:${N} the docker daemon isn't reachable - is it running, and are you root or in the 'docker' group?"
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "${R}error:${N} 'docker compose' plugin not found. Install the pinned v5.x plugin"
echo " binary per the bootstrap steps in README.md (distro packages such as"
echo " docker-compose-v2 ship a different major and won't pass the check below)."
exit 1
fi
# Compose MAJOR version must match the in-stack updater's compose. The
# updater reconciles the stack's networks on every upgrade and per-run
# worker recycle; compose stamps a per-network "config hash" that differs
# across major versions, so if the host compose that first creates the
# networks is a different major than the updater's, the updater's `up`
# tries to recreate networks that still have containers attached and
# fails ("network ... has active endpoints"). Keep REQUIRED_COMPOSE_MAJOR
# in lockstep with COMPOSE_VERSION in build/updater.Dockerfile.
REQUIRED_COMPOSE_MAJOR=5
HOST_COMPOSE_VER=$(docker compose version --short 2>/dev/null | sed 's/^v//')
HOST_COMPOSE_MAJOR=${HOST_COMPOSE_VER%%.*}
if [ -n "$HOST_COMPOSE_MAJOR" ] && [ "$HOST_COMPOSE_MAJOR" != "$REQUIRED_COMPOSE_MAJOR" ]; then
echo "${R}error:${N} Docker Compose v${REQUIRED_COMPOSE_MAJOR}.x is required (found v${HOST_COMPOSE_VER:-unknown})."
echo " The in-stack updater runs Compose v${REQUIRED_COMPOSE_MAJOR}.x and reconciles the stack's"
echo " networks on upgrades; a different major makes it try to recreate networks"
echo " that have active endpoints, which fails. Install the pinned v${REQUIRED_COMPOSE_MAJOR}.x plugin"
echo " binary per the bootstrap steps in README.md."
exit 1
fi
# --- prompts ---
echo "${B}Ghost Agent Platform - setup${N}"
echo
# Docker Hub OAT (collected first so the newest release tag can be
# resolved from the registry before the tag prompt).
read -r -s -p "Docker Hub OAT (will be hidden): " DOCKER_OAT
echo
[ -z "$DOCKER_OAT" ] && { echo "${R}error:${N} OAT is required"; exit 1; }
# Resolve the newest published release tag from the registry - the same
# semantics as the in-stack updater's poller and the AWS bootstrap.
# exo-stack is published LAST in the release pipeline (after every
# image), so its newest clean-semver tag is a fully published release.
# Best-effort: a lookup failure just leaves the prompt without a default.
REGISTRY_VALUE="${REGISTRY:-docker.io/ghostsecurityhq}"
DH_ORG="${REGISTRY_VALUE##*/}"
ORAS_IMAGE="ghcr.io/oras-project/oras:v1.2.0"
echo
echo "Resolving newest release..."
LATEST_TAG=$(docker run --rm "$ORAS_IMAGE" \
repo tags --username "$DH_ORG" --password "$DOCKER_OAT" \
"${REGISTRY_VALUE}/exo-stack" 2>/dev/null \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V | tail -1 || true)
# Release tag - defaults to the resolved latest; the operator can pin a
# specific version by typing it.
if [ -n "$LATEST_TAG" ]; then
read -r -p "Release tag to deploy [${LATEST_TAG}]: " TAG
TAG="${TAG:-$LATEST_TAG}"
else
echo "${Y}note:${N} could not resolve the latest tag automatically."
read -r -p "Release tag to deploy (e.g. v0.0.45): " TAG
fi
[ -z "$TAG" ] && { echo "${R}error:${N} TAG is required"; exit 1; }
# --- generate ---
# The one secret this script delivers: the claim token that unlocks the
# in-product setup wizard. The platform stores only its hash; the raw
# value is printed once below.
CLAIM_TOKEN=$(openssl rand -hex 32)
# Detect the public IP: its nip.io name becomes the bring-up hostname
# the pre-setup Caddyfile serves with a real Let's Encrypt cert, so the
# wizard loads without a certificate warning. Detection failure is
# non-fatal - the catch-all self-signed fallback still serves the
# wizard on the bare IP (one browser warning to accept).
DETECTED_IP=$(curl -4 -fsS --max-time 5 https://checkip.amazonaws.com 2>/dev/null | tr -d '[:space:]' || true)
BRINGUP_DOMAIN=""
if [ -n "$DETECTED_IP" ]; then
BRINGUP_DOMAIN="${DETECTED_IP//./-}.nip.io"
fi
# Minimal .env: image selection, registry auth, the claim token, and
# the bring-up hostname. Everything else (domain, TLS, connectors,
# worker count) is configured through the setup wizard and rendered
# into this file by the platform.
cat > .env <<EOF
# Ghost Agent Platform - runtime environment.
#
# Written by setup.sh; the platform rewrites managed lines in this file
# when instance settings change. Lines you add for the optional
# overrides documented in .env.example are preserved.
TAG=${TAG}
EXO_UPDATER_OCI_AUTH_TOKEN=${DOCKER_OAT}
EXO_CLAIM_TOKEN=${CLAIM_TOKEN}
EOF
if [ -n "$BRINGUP_DOMAIN" ]; then
printf 'EXO_BRINGUP_DOMAIN=%s\n' "$BRINGUP_DOMAIN" >> .env
fi
chmod 600 .env # holds secrets: the OAT and the claim token
# --- fetch the stack bundle ---
# The docker-compose.yml is NOT shipped in this repo. It's published per
# release as the OCI "stack bundle" `${REGISTRY}/exo-stack:${TAG}`,
# together with the static config defaults (defaults/), the Caddyfile
# templates the platform renders settings into (templates/), and host
# helper scripts (scripts/). The in-stack updater fetches subsequent
# versions on each topology-aware upgrade (same source of truth). We
# pull with a throwaway `oras` container (no host oras install needed),
# authenticating with the OAT already collected above.
REGISTRY_VALUE="${REGISTRY:-docker.io/ghostsecurityhq}"
DH_ORG="${REGISTRY_VALUE##*/}"
STACK_REF="${REGISTRY_VALUE}/exo-stack:${TAG}"
ORAS_IMAGE="ghcr.io/oras-project/oras:v1.2.0"
echo
echo "${B}Fetching stack bundle${N} ${STACK_REF}"
if docker run --rm -v "$PWD:/work" -w /work "$ORAS_IMAGE" \
pull --username "$DH_ORG" --password "$DOCKER_OAT" "$STACK_REF" -o . ; then
echo " wrote docker-compose.yml + defaults/ + templates/ + scripts/"
else
echo "${R}error:${N} failed to fetch the stack bundle ${STACK_REF}."
echo " Confirm the tag exists in Docker Hub and the OAT has read access, then re-run."
exit 1
fi
for f in defaults/config.toml defaults/config.proxy.toml defaults/Caddyfile.bootstrap; do
if [ ! -f "$f" ]; then
echo "${R}error:${N} bundle is missing $f - the release predates the setup wizard."
echo " Deploy a newer release tag."
exit 1
fi
done
# --- place the config defaults ---
# Copy-if-absent only: neither this script nor an upgrade ever
# overwrites the live copies. The platform (updater) rewrites the
# Caddyfile and managed .env lines when instance settings change.
cp -n defaults/config.toml config.toml
cp -n defaults/config.proxy.toml config.proxy.toml
cp -n defaults/Caddyfile.bootstrap Caddyfile
# BYO-cert drop point; bind-mounted into the edge proxy. The platform
# writes operator-uploaded certs here when custom TLS is selected.
mkdir -p certs
# config.toml and config.proxy.toml carry no secrets and are
# bind-mounted read-only into the non-root gateway / credential-proxy
# (UID 65532); keep them world-readable so the containers can read them
# regardless of the operator's umask.
chmod 644 config.toml config.proxy.toml Caddyfile
# --- claim-token reissue helper ---
# Installs the on-host helper that rotates the claim token for an
# unclaimed instance (lost/exposed token before the wizard ran).
if [ -f scripts/reissue-claim-token.sh ]; then
if [ "$(id -u)" -eq 0 ]; then
install -m 755 scripts/reissue-claim-token.sh /usr/local/bin/exo-reissue-claim-token
echo " installed /usr/local/bin/exo-reissue-claim-token"
elif command -v sudo >/dev/null 2>&1; then
sudo install -m 755 scripts/reissue-claim-token.sh /usr/local/bin/exo-reissue-claim-token
echo " installed /usr/local/bin/exo-reissue-claim-token"
else
echo "${Y}note:${N} could not install the reissue helper (no root/sudo);"
echo " run scripts/reissue-claim-token.sh directly if the claim token is lost."
fi
fi
# --- summary ---
if [ -n "$BRINGUP_DOMAIN" ]; then
WIZARD_URL="https://${BRINGUP_DOMAIN}"
else
WIZARD_URL="https://<this-host's-public-IP>"
fi
echo
echo "${G}done${N} - configuration written to .env, config.toml, config.proxy.toml, Caddyfile"
echo
echo "${Y}Claim token (needed once, in the setup wizard):${N}"
echo
echo " ${B}${CLAIM_TOKEN}${N}"
echo
echo "${Y}Save it until setup completes - it won't be shown again.${N}"
echo "(Lost it before claiming? Run exo-reissue-claim-token to rotate.)"
# --- host tuning (optional) ---
echo
echo "${B}Host tuning${N} (optional, modifies system files - needs root/sudo):"
echo " - Cap container log size at 10MB x 3 files (json-file driver)"
echo " - Daily prune of unused images older than 7 days"
echo
read -r -p "Apply? [Y/n]: " TUNE_CHOICE
case "${TUNE_CHOICE:-Y}" in
[nN]|[nN][oO])
echo "Skipping host tuning."
;;
*)
# Pick the privilege escalator. Skip with a notice if we're not
# root AND sudo isn't installed.
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
elif command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
echo "${Y}note:${N} need root or sudo to apply host tuning - skipping"
TUNE_SKIPPED=1
fi
DOCKER_BIN=$(command -v docker || echo /usr/bin/docker)
if [ -z "${TUNE_SKIPPED:-}" ]; then
# Docker daemon log rotation. Refuse to overwrite an existing
# daemon.json - operators may have other config there. The
# warning tells them what to add manually.
if [ -f /etc/docker/daemon.json ]; then
echo "${Y}note:${N} /etc/docker/daemon.json already exists - skipping log rotation."
echo " To enable manually, add:"
echo ' "log-driver": "json-file",'
echo ' "log-opts": { "max-size": "10m", "max-file": "3" }'
else
$SUDO mkdir -p /etc/docker
$SUDO tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
# Restart docker so the new log driver picks up. Safe here
# because the stack hasn't been brought up yet (setup.sh
# runs before `docker compose up`).
$SUDO systemctl restart docker
echo " wrote /etc/docker/daemon.json and restarted docker"
fi
# Systemd timer for daily image prune. Idempotent - re-writing
# the same content on a re-run is fine.
$SUDO tee /etc/systemd/system/exo-docker-prune.service > /dev/null <<EOF
[Unit]
Description=Prune unused Docker images older than 7 days
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=${DOCKER_BIN} image prune -a --filter "until=168h" -f
EOF
$SUDO tee /etc/systemd/system/exo-docker-prune.timer > /dev/null <<'EOF'
[Unit]
Description=Daily Docker image prune
[Timer]
OnCalendar=daily
# Persistent=true catches up missed runs (host was off, etc.) on
# next boot instead of waiting another 24h.
Persistent=true
[Install]
WantedBy=timers.target
EOF
$SUDO systemctl daemon-reload
$SUDO systemctl enable --now exo-docker-prune.timer >/dev/null 2>&1
echo " installed exo-docker-prune.timer (daily image prune)"
fi
;;
esac
echo
echo "Next:"
echo " docker login -u ghostsecurityhq # if you haven't already"
echo " docker compose pull"
echo " docker compose up -d"
echo
echo "Then open ${B}${WIZARD_URL}${N} to run the setup wizard and enter the"
echo "claim token above. The wizard creates the admin account and"
echo "configures the domain and TLS."
if [ -z "$BRINGUP_DOMAIN" ]; then
echo "(Browsing by bare IP serves a temporary self-signed certificate -"
echo "accept the one-time warning.)"
fi