#!/usr/bin/env bash # monky-deployd installer — Ubuntu 26.04 (verified target). # # T= # from the install kit / OpenBao # curl -sSf -H "DEPLOY-TOKEN: $T" \ # https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd/0.1.5/install.sh \ # | sudo bash -s -- --env env-qa-02 --site cbs --token "$T" --bootstrap-file bootstrap.jwt \ # [--transport sdk|proxy|system] [--version 0.1.5] [--enrol-jwt /path/monky-host.env-qa-02.jwt] \ # [--laptop] [--source gitlab|gitea] [--docker-data-root /home/docker-data] # # --token / MONKY_DEPLOYD_TOKEN: the GitLab project is PRIVATE (its parent groups are private, so it # cannot be made public); every download from the generic package registry — this script included — # sends `DEPLOY-TOKEN: `. The token is read-only (read_package_registry), revocable, and is # never printed by this script. Not needed with --source gitea. # --bootstrap-file (or stdin when the script runs from a FILE): the ONE-TIME bootstrap deploy grant # from the install kit (GET /v1/backends/{id}/agent/install). The enrolment JWT is read from # --enrol-jwt or /etc/monky-deployd/enrol.jwt and is only needed when the host identity is not # enrolled yet. # # What it does (idempotent): # 1. apt: ziti-edge-tunnel (OpenZiti `jammy` suite) if absent, docker-compose-plugin, acl # 2. downloads the pinned monky-deployd__amd64.deb + .sha256 from the scm.tikali.ai package registry # (DEPLOY-TOKEN header; --source gitea: the Gitea release, off-estate), verifies, installs # 3. enrols /opt/openziti/etc/identities/monky-host..json if absent (ziti-edge-tunnel enroll), # chown ziti:ziti 0600, switches ziti-edge-tunnel.service to `run-host` via a drop-in # 4. writes /etc/monky-deployd/config.yaml, ACLs so user monky-deployd can read the identity, # the bootstrap grant 0600, (proxy transport: proxy.env + monky-deployd-proxy.service) # 5. enables monky-deployd.timer, runs one tick, deletes the enrol JWT, prints the checklist set -euo pipefail umask 077 DEFAULT_VERSION="0.1.5" # Download source. PRIMARY is the GitLab project's generic package registry on scm.tikali.ai: inside # the estate gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175), which has no HTTP # ingress, so backend boxes cannot reach the Gitea mirror (cbs/iac#102); scm.tikali.ai they can. The # project is PRIVATE (parent groups are private), so the registry needs the read-only deploy token # (--token / MONKY_DEPLOYD_TOKEN, sent as the DEPLOY-TOKEN header). `--source gitea` keeps the Gitea # release as the off-estate alternative (no token). --base-url / MONKY_DEPLOYD_BASE_URL override the # base for whichever layout is selected. GITLAB_BASE_URL="https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd" GITEA_BASE_URL="https://gitea.cbs.tikali.net/mdella/monky-deployd" SOURCE="${MONKY_DEPLOYD_SOURCE:-gitlab}" BASE_URL="${MONKY_DEPLOYD_BASE_URL:-}" TOKEN="${MONKY_DEPLOYD_TOKEN:-}" OPENZITI_SUITE="${OPENZITI_SUITE:-jammy}" IDENTITY_DIR="/opt/openziti/etc/identities" ETC="/etc/monky-deployd" ENV_ID="" SITE="" TRANSPORT="sdk" DOCKER_DATA_ROOT="" TUNNEL_FAILED="" DEPLOYD_VERSION="$DEFAULT_VERSION" ENROL_JWT="" BOOTSTRAP_FILE="" NO_RUN="" FORCE_CONFIG="" BAO_CA="" LAPTOP="false" usage() { if [ -f "$0" ]; then sed -n '2,28p' "$0"; else echo "monky-deployd install.sh — see README.md (Install)"; fi; exit "${1:-0}"; } die() { echo "install.sh: $*" >&2; exit 1; } log() { echo "==> $*"; } while [ $# -gt 0 ]; do case "$1" in --env) ENV_ID="$2"; shift 2 ;; --site) SITE="$2"; shift 2 ;; --transport) TRANSPORT="$2"; shift 2 ;; --version) DEPLOYD_VERSION="$2"; shift 2 ;; --docker-data-root) DOCKER_DATA_ROOT="$2"; shift 2 ;; --enrol-jwt) ENROL_JWT="$2"; shift 2 ;; --bootstrap-file) BOOTSTRAP_FILE="$2"; shift 2 ;; --bao-ca) BAO_CA="$2"; shift 2 ;; --base-url) BASE_URL="$2"; shift 2 ;; --source) SOURCE="$2"; shift 2 ;; --token) TOKEN="$2"; shift 2 ;; --laptop) LAPTOP="true"; shift ;; --no-run) NO_RUN=1; shift ;; --force-config) FORCE_CONFIG=1; shift ;; -h|--help) usage 0 ;; *) die "unknown argument $1 (see --help)" ;; esac done [ "$(id -u)" = 0 ] || die "run as root (sudo)" [ -n "$ENV_ID" ] || die "--env is required" [ -n "$SITE" ] || die "--site is required" [[ "$ENV_ID" =~ ^env-(dev|qa|stage|prod)-[0-9]{2,3}$|^(dev-env-2|prod-cedar)$ ]] || die "env id $ENV_ID is not env--" SITE="${SITE,,}" [[ "$SITE" =~ ^(cbs|pdx)$ ]] || die "site must be cbs|pdx" [[ "$TRANSPORT" =~ ^(sdk|proxy|system)$ ]] || die "transport must be sdk|proxy|system" [[ "$SOURCE" =~ ^(gitlab|gitea)$ ]] || die "source must be gitlab|gitea" # never let the token leak through xtrace / the environment of children { set +x; } 2>/dev/null export -n MONKY_DEPLOYD_TOKEN 2>/dev/null || true IDENTITY="$IDENTITY_DIR/monky-host.$ENV_ID.json" export DEBIAN_FRONTEND=noninteractive if [ -r /etc/os-release ]; then # read os-release in a SUBSHELL: it defines VERSION/NAME/ID/... and sourcing it inline clobbered # this script's own variables (0.1.2 → "26.04 LTS (Resolute Raccoon)" on the env-qa-02 pilot) OS_ID=$(. /etc/os-release; printf '%s' "${ID:-}") OS_VERSION_ID=$(. /etc/os-release; printf '%s' "${VERSION_ID:-}") OS_CODENAME=$(. /etc/os-release; printf '%s' "${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}") if [ "$OS_ID" != "ubuntu" ] || [ "$OS_VERSION_ID" != "26.04" ]; then echo "WARNING: verified on Ubuntu 26.04 only (this is ${OS_ID:-?} ${OS_VERSION_ID:-?}); continuing" >&2 fi fi # --- 1. packages ------------------------------------------------------------------------------------- apt_updated="" apt_update_once() { [ -n "$apt_updated" ] || { apt-get update -qq; apt_updated=1; }; } need_pkgs=(acl curl ca-certificates gnupg) if ! command -v ziti-edge-tunnel >/dev/null 2>&1; then log "adding the OpenZiti apt repository ($OPENZITI_SUITE suite)" install -d -m 0755 /usr/share/keyrings curl -fsSL https://get.openziti.io/tun/package-repos.gpg | gpg --dearmor -o /usr/share/keyrings/openziti.gpg chmod 0644 /usr/share/keyrings/openziti.gpg echo "deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable $OPENZITI_SUITE main" \ > /etc/apt/sources.list.d/openziti.list need_pkgs+=(ziti-edge-tunnel) fi if [ "$TRANSPORT" = "proxy" ] && ! command -v ziti >/dev/null 2>&1; then need_pkgs+=(openziti) # the `ziti` CLI (ziti tunnel proxy) from the same repo fi if ! command -v docker >/dev/null 2>&1; then # Docker Engine from Docker's apt repo (the suite for this Ubuntu codename); falls back to Ubuntu's # docker.io if Docker has no suite for the codename yet. --docker-data-root puts the data-root on # a dedicated disk BEFORE the daemon first starts (pilot VMs mount one at /home/docker-data). log "docker is not installed — installing Docker Engine" export DEBIAN_FRONTEND=noninteractive install -d -m 0755 /etc/apt/keyrings if curl -fsSL --max-time 20 "https://download.docker.com/linux/ubuntu/dists/${OS_CODENAME:-noble}/Release" -o /dev/null 2>/dev/null; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${OS_CODENAME:-noble} stable" > /etc/apt/sources.list.d/docker.list apt-get update -qq docker_pkgs="docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" else log "Docker has no apt suite for '${OS_CODENAME:-?}' yet — using Ubuntu's docker.io" apt-get update -qq docker_pkgs="docker.io docker-compose-v2" fi if [ -n "$DOCKER_DATA_ROOT" ]; then install -d -m 0710 "$DOCKER_DATA_ROOT" install -d -m 0755 /etc/docker [ -f /etc/docker/daemon.json ] || printf '{ "data-root": "%s" }\n' "$DOCKER_DATA_ROOT" > /etc/docker/daemon.json fi # shellcheck disable=SC2086 apt-get install -y -qq --no-install-recommends $docker_pkgs systemctl enable --now docker log "installed $(docker --version) (data-root $(docker info -f '{{.DockerRootDir}}' 2>/dev/null || echo default))" fi docker compose version >/dev/null 2>&1 || need_pkgs+=(docker-compose-plugin) python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) else 1)' 2>/dev/null || die "python3 >= 3.12 is required" if [ "${#need_pkgs[@]}" -gt 0 ]; then log "apt install: ${need_pkgs[*]}" apt_update_once apt-get install -y -qq --no-install-recommends "${need_pkgs[@]}" fi # --- 2. the pinned .deb ----------------------------------------------------------------------------- installed="$(dpkg-query -W -f='${Version}' monky-deployd 2>/dev/null || true)" if [ "$installed" = "$DEPLOYD_VERSION" ]; then log "monky-deployd $DEPLOYD_VERSION already installed" else tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT deb="monky-deployd_${DEPLOYD_VERSION}_amd64.deb" # the deploy token travels in a 0600 curl config file (-K), never on the command line, so neither # `ps` nor an xtrace shows it; the file dies with $tmp curlrc="$tmp/curlrc"; : > "$curlrc"; chmod 0600 "$curlrc" if [ "$SOURCE" = gitlab ]; then url="${BASE_URL:-$GITLAB_BASE_URL}/${DEPLOYD_VERSION}" if [ -n "$TOKEN" ]; then printf 'header = "DEPLOY-TOKEN: %s"\n' "$TOKEN" > "$curlrc" else echo "WARNING: no --token/MONKY_DEPLOYD_TOKEN: the scm.tikali.ai project is private, the download will 401 (use --source gitea off-estate)" >&2 fi else url="${BASE_URL:-$GITEA_BASE_URL}/releases/download/v${DEPLOYD_VERSION}" fi log "downloading $deb from $url${TOKEN:+ (DEPLOY-TOKEN)}" curl -fsSL -K "$curlrc" -o "$tmp/$deb" "$url/$deb" || die "download of $deb failed (private registry: is the deploy token set and valid?)" curl -fsSL -K "$curlrc" -o "$tmp/$deb.sha256" "$url/$deb.sha256" || die "download of $deb.sha256 failed" rm -f "$curlrc" (cd "$tmp" && sha256sum -c "$deb.sha256") || die "sha256 mismatch on $deb" apt_update_once || true apt-get install -y -qq "$tmp/$deb" fi command -v monky-deployd >/dev/null || die "monky-deployd not on PATH after install" # --- 3. host identity ------------------------------------------------------------------------------- install -d -m 0750 "$IDENTITY_DIR" [ -n "$ENROL_JWT" ] || { [ -f "$ETC/enrol.jwt" ] && ENROL_JWT="$ETC/enrol.jwt"; } || true if [ -s "$IDENTITY" ]; then log "host identity present: $IDENTITY" else [ -n "$ENROL_JWT" ] && [ -s "$ENROL_JWT" ] || die "no identity at $IDENTITY and no enrol JWT (--enrol-jwt or $ETC/enrol.jwt)" log "enrolling monky-host.$ENV_ID" ziti-edge-tunnel enroll -j "$ENROL_JWT" -i "$IDENTITY" fi getent passwd ziti >/dev/null && chown ziti:ziti "$IDENTITY" || true chmod 0600 "$IDENTITY" # ziti-edge-tunnel as a HOST (bind side, no tun/DNS) — the agent dials with the SDK or the proxy install -d /etc/systemd/system/ziti-edge-tunnel.service.d cat > /etc/systemd/system/ziti-edge-tunnel.service.d/run-host.conf <<'DROPIN' # monky-deployd: run-host mode (no tun, no DNS); identities from the standard directory. # ExecStartPre is cleared: the package's ziti-edge-tunnel.sh (auto-enrol *.jwt in the identity dir) # is not executable by user ziti on 1.18.x (203/EXEC, env-qa-02 pilot) and run-host does not need it. [Service] ExecStartPre= ExecStart= ExecStart=/opt/openziti/bin/ziti-edge-tunnel run-host --identity-dir=/opt/openziti/etc/identities DROPIN systemctl daemon-reload systemctl enable ziti-edge-tunnel.service >/dev/null 2>&1 || true if ! systemctl restart ziti-edge-tunnel.service; then # do NOT abort here: config, ACLs, grant and timer below must land so a re-run needs no new kit echo "WARNING: ziti-edge-tunnel.service failed to start — see: journalctl -u ziti-edge-tunnel; continuing" >&2 TUNNEL_FAILED=1 fi # --- 4. config, ACLs, bootstrap grant ----------------------------------------------------------------- install -d -m 0750 -o root -g monky-deployd "$ETC" install -d -m 0700 -o monky-deployd -g monky-deployd /var/lib/monky-deployd setfacl -m u:monky-deployd:r "$IDENTITY" setfacl -m u:monky-deployd:rx "$IDENTITY_DIR" setfacl -m u:monky-deployd:x /opt/openziti/etc 2>/dev/null || true if [ -n "$BAO_CA" ]; then install -m 0644 "$BAO_CA" "$ETC/openbao-ca.pem" fi ca_line="ca_bundle: $ETC/openbao-ca.pem" [ -s "$ETC/openbao-ca.pem" ] || { ca_line="ca_bundle: none # TODO: install the openbao-ca certificate (see docs/OPERATIONS.md)"; echo "WARNING: $ETC/openbao-ca.pem missing; TLS to OpenBao will use the system store" >&2; } if [ -s "$ETC/config.yaml" ] && [ -z "$FORCE_CONFIG" ]; then log "keeping existing $ETC/config.yaml (use --force-config to rewrite)" else cat > "$ETC/config.yaml" < "$ETC/proxy.env"; chmod 0644 "$ETC/proxy.env" systemctl enable --now monky-deployd-proxy.service fi # bootstrap grant: --bootstrap-file, or stdin — but ONLY when this script runs from a file: with # `curl … | bash -s --` stdin IS the script, so reading it here would swallow the rest of it grant="" if [ -n "$BOOTSTRAP_FILE" ]; then grant="$(tr -d '[:space:]' < "$BOOTSTRAP_FILE")" elif [ ! -t 0 ] && [ -f "$0" ]; then grant="$(tr -d '[:space:]' "$ETC/bootstrap.jwt" chown monky-deployd:monky-deployd "$ETC/bootstrap.jwt"; chmod 0600 "$ETC/bootstrap.jwt" log "bootstrap grant staged at $ETC/bootstrap.jwt (consumed on first tick)" elif [ -s /var/lib/monky-deployd/bao.token ]; then log "no bootstrap grant given; existing bao.token kept" else echo "WARNING: no bootstrap grant on stdin and no bao.token: the agent cannot check in until you provide one" >&2 fi unset grant TOKEN # --- 5. timer + first tick ---------------------------------------------------------------------------------- systemctl daemon-reload systemctl enable --now monky-deployd.timer if [ -z "$NO_RUN" ]; then log "first tick" systemctl start monky-deployd.service || true fi [ -n "$ENROL_JWT" ] && rm -f "$ENROL_JWT" || true rm -f "$ETC/enrol.jwt" cat <= 3.12 docker compose version # compose plugin present systemctl is-active ziti-edge-tunnel # run-host mode (drop-in run-host.conf) $( [ "$TRANSPORT" = proxy ] && echo "systemctl is-active monky-deployd-proxy # ziti tunnel proxy on 18443/18200" || echo "ziti tunnel proxy --help >/dev/null # fallback transport available" ) /opt/monky-deployd/venv/bin/python -c 'import openziti' # SDK import (transport sdk) monky-deployd status # token present, applied == desired journalctl -u monky-deployd -n 50 # 'checkin:' and 'applied' lines df -h \$(docker info -f '{{.DockerRootDir}}') # free space >= bundle need x 1.5 + headroom docker compose -p monky-$ENV_ID ps # healthy # from another mesh member: curl monky.percept.$ENV_ID:47283/health -> 200 CHECK CHECK_STATUS="$(monky-deployd status 2>&1 || true)" echo "$CHECK_STATUS" | sed 's/^/ | /' if [ -n "$TUNNEL_FAILED" ] && ! systemctl is-active --quiet ziti-edge-tunnel.service; then echo "ERROR: ziti-edge-tunnel.service is not running; the agent cannot reach the mesh until it is. Everything else is installed — fix the unit and 'systemctl restart ziti-edge-tunnel monky-deployd'." >&2 exit 1 fi