#!/usr/bin/env bash # # Firecradle Agent — one-command installer for Linux. # # Published to the CDN at, and only at: # # https://cdn.shadowcradle.com/download/fc-agent/linux/stable/latest/install.sh # # It is generated by nothing: this file is the published file, byte for byte, # and `python3 sandbox.py publish-stage` copies it. If you are reading it from # the CDN and want to know it is ours, read it here instead — # build/installer/cdn/install.sh in git.takelan.com/shadowcradle/firecradle-agent. # # NOT build/installer/linux/install.sh. That one installs from an unpacked # tree and is what this script ends up running; this one is the bootstrap that # downloads the tree first. # # # WHAT THE ONE-LINER TRUSTS, PLAINLY # ---------------------------------- # curl -fsSL https://cdn.shadowcradle.com/.../install.sh | sudo bash # # is offered because it is what people will do anyway, and this is a truthful # account of it. Piping to `sudo bash` means: # # * you run whatever the CDN serves at that moment, as root, without reading # it. TLS proves you reached cdn.shadowcradle.com. It does not prove that # what cdn.shadowcradle.com is serving today is what was reviewed; # * this script then verifies the installer's SHA-256 against a digest it # fetches from THE SAME BUCKET. That catches a corrupted or truncated # download. It catches nothing at all about someone who can write to the # bucket, because they would write both; # * the release is NOT SIGNED. There is no pinned key in this build, so # there is no check anywhere in this path that survives a bad bucket. # # THE TWO-STEP, WHICH IS THE ONE TO USE IF YOU CARE: # # curl -fsSLO https://cdn.shadowcradle.com/.../install.sh # curl -fsSLO https://cdn.shadowcradle.com/.../install.sh.sha256 # sha256sum -c install.sh.sha256 # the published digest # less install.sh # read it # sudo bash install.sh # # These are not equivalent and this script will not pretend they are. # # # WHAT THE DIGEST BESIDE THIS SCRIPT IS WORTH, AND WHAT IT IS NOT # --------------------------------------------------------------- # This script is published with an `install.sh.sha256` beside it, like every # other object in the release. # # THAT DIGEST DOES NOTHING FOR THE ONE-LINER. A pipe into a shell consumes the # bytes as they arrive and never has them again, so there is no second copy to # hash and nothing to compare against -- by the time you could check, the # script has already run as root. The digest is for the two-step above, where # you fetch the file, keep it, and check it before running it, and for a mirror # that stores this script and re-checks it later. It is not what makes the # one-liner safe, because nothing does. # # And it is served from the same bucket by the same host as this script, so # like every other digest in this release it is evidence about corruption and # not about origin. Comparing it with `build/installer/cdn/install.sh` in git # is the check that is worth something. # # # THERE IS NO ENROLMENT SECRET IN HERE, AND THERE WILL NOT BE # ----------------------------------------------------------- # This script installs the agent. It does not enrol it. Sign-in is the RFC 8628 # device authorization grant: the tray opens a verification URL, a person signs # in in their own browser, and the device gets its own revocable credential. # # A provisioning key is an ORG-WIDE credential — holding one is enough to enrol # new devices claiming any principal. Putting one in a script that is published # to a CDN and pasted into terminals would publish it to every desktop that # ever runs the command, and to the shell history of every one of them. This # script REFUSES such a flag rather than accepting it quietly; managed # enrolment has its own channel per platform, documented in # build/installer/INSTALLER-NOTES.md, and on Linux it is a root-owned 0600 file # the service reads once and deletes. set -euo pipefail BASE="${FIRECRADLE_CDN_BASE:-https://cdn.shadowcradle.com}" # The product is the first path segment and the PLATFORM SLUG is the second. # The slug is the sibling's spelling, not the build platform's: `linux` happens # to be the same word, and `windows` is published as `win`. build/publish.py's # PLATFORM_SLUGS is the table. This script is the Linux one, so its slug is # fixed here rather than detected: the arch is a variable, the platform is # which file you are reading. PRODUCT="fc-agent" PLATFORM="linux" CHANNEL="stable" VERSION="" ASSUME_YES=0 FORCE=0 KEEP=0 fail() { # Every exit path says what happened in a sentence, and the exit status # says which kind. A bootstrap that dies on `set -e` with no output is one # nobody can support. echo "" >&2 echo "Firecradle Agent was NOT installed." >&2 echo "$2" >&2 exit "$1" } usage() { cat <<'USAGE' Firecradle Agent installer. sudo bash install.sh [options] --channel stable|beta Which release channel to install from (default: stable) --version X.Y.Z Install this exact version instead of the latest --yes, -y Do not ask for confirmation --force Reinstall even if this version is already installed --keep-download Leave the downloaded archive in place for inspection --help This text It downloads the installer for this machine, checks the SHA-256 the CDN publishes beside it, and runs it. It does not enrol the device: you sign in from the tray, in a browser, after installing. USAGE } while [[ $# -gt 0 ]]; do case "$1" in --channel) CHANNEL="${2:-}"; shift 2 ;; --channel=*) CHANNEL="${1#*=}"; shift ;; --version) VERSION="${2:-}"; shift 2 ;; --version=*) VERSION="${1#*=}"; shift ;; -y|--yes) ASSUME_YES=1; shift ;; --force) FORCE=1; shift ;; --keep-download) KEEP=1; shift ;; -h|--help) usage; exit 0 ;; --enrollment-key*|--enrolment-key*|--provisioning-key*|--enrollment-code*|--enrolment-code*|--org-key*|--token*) fail 2 "That option is not accepted, on purpose. A provisioning key is an organisation-wide credential: whoever holds it can enrol new devices claiming any principal. Passing one to a script that is published on a CDN and pasted into terminals puts it in the shell history of every machine it is run on, and in any log that captures a command line. To install one device, run this script with no key and sign in from the tray: Firecradle uses a browser device-code login, so the device gets its own credential that your administrator can revoke on its own. To deploy a fleet, use the managed-enrolment channel for this platform, which places a root-only 0600 file the service reads once and deletes. See build/installer/INSTALLER-NOTES.md, 'Managed enrolment'." ;; *) fail 2 "Unknown option: $1. Run with --help to see what is accepted." ;; esac done case "$CHANNEL" in stable|beta) ;; *) fail 2 "'$CHANNEL' is not a release channel. Use stable or beta." ;; esac [[ $EUID -eq 0 ]] || fail 3 "This installer needs root: it registers a systemd service and creates a system group. Try: sudo bash $0 $*" command -v curl >/dev/null 2>&1 \ || fail 4 "'curl' is not installed and this script needs it to download anything. Install it (apt install curl / dnf install curl) and run this again." # The digest is the ONLY integrity check in this path, so a machine that cannot # compute one does not get an install. Downgrading to "install it anyway" here # would make the check decorative on exactly the hosts where it is missing. if command -v sha256sum >/dev/null 2>&1; then SHA256_CMD=(sha256sum) elif command -v shasum >/dev/null 2>&1; then SHA256_CMD=(shasum -a 256) else fail 4 "Neither 'sha256sum' nor 'shasum' is installed. The published SHA-256 is the only integrity check this installer has, so it will not install an artefact it cannot check. Install coreutils and run this again." fi case "$(uname -m)" in x86_64|amd64) ARCH="x86_64" ;; aarch64|arm64) ARCH="arm64" ;; *) fail 4 "There is no Firecradle Agent build for $(uname -m). Published builds are x86_64 and arm64." ;; esac DOWNLOAD="${BASE}/download/${PRODUCT}/${PLATFORM}/${CHANNEL}" if [[ -z "$VERSION" ]]; then VERSION="$(curl -fsSL "${DOWNLOAD}/latest/VERSION" 2>/dev/null | tr -d '[:space:]')" \ || fail 5 "Could not ask ${BASE} which version is current. Check the machine's network and proxy settings, then run this again." fi [[ -n "$VERSION" ]] || fail 5 "${DOWNLOAD}/latest/VERSION was empty, so there is nothing to install. That is a publishing fault, not a fault on this machine — please report it." [[ "$VERSION" =~ ^[0-9][0-9A-Za-z.]*([-+][0-9A-Za-z.]+)*$ ]] \ || fail 5 "'${VERSION}' is not a version number. Refusing to build a download URL out of it." FILE="firecradle-agent-${VERSION}-linux-${ARCH}.tar.gz" URL="${DOWNLOAD}/${VERSION}/${FILE}" PREFIX="${PREFIX:-/opt/firecradle-agent}" # Idempotence: already at this version and not forced -> say so and stop, with # a zero status. Re-running the command in a configuration-management loop must # not reinstall the world every fifteen minutes. INSTALLED="" if [[ -x "${PREFIX}/firecradle-agent" ]]; then INSTALLED="$("${PREFIX}/firecradle-agent" --version 2>/dev/null | tr -d '[:space:]' || true)" fi if [[ -n "$INSTALLED" && "$INSTALLED" == "$VERSION" && $FORCE -eq 0 ]]; then echo "Firecradle Agent ${VERSION} is already installed at ${PREFIX}." echo "Nothing to do. Pass --force to reinstall it anyway." exit 0 fi cat < /dev/tty read -r answer < /dev/tty || answer="" else fail 6 "This is running without a terminal to ask on (a pipe, or an automation runner). Nothing was changed. Re-run it with --yes if you have read the plan above: curl -fsSL ${DOWNLOAD}/latest/install.sh | sudo bash -s -- --yes" fi case "$answer" in y|Y|yes|YES) ;; *) echo "Nothing was changed."; exit 0 ;; esac fi WORK="$(mktemp -d)" cleanup() { if [[ $KEEP -eq 1 ]]; then echo "Downloaded files kept in ${WORK}" else rm -rf "$WORK" fi } trap cleanup EXIT echo "==> Downloading ${FILE}" curl -fsSL --retry 3 --retry-delay 2 -o "${WORK}/${FILE}" "$URL" \ || fail 7 "The download failed: ${URL} If you passed --version, check that this version was published for ${ARCH}." echo "==> Fetching the published SHA-256" curl -fsSL --retry 3 --retry-delay 2 -o "${WORK}/${FILE}.sha256" "${URL}.sha256" \ || fail 7 "The download succeeded and its checksum did not: ${URL}.sha256 Refusing to install an artefact that cannot be checked." EXPECTED="$(awk 'NR==1{print tolower($1)}' "${WORK}/${FILE}.sha256")" ACTUAL="$("${SHA256_CMD[@]}" "${WORK}/${FILE}" | awk '{print tolower($1)}')" if [[ -z "$EXPECTED" || ! "$EXPECTED" =~ ^[0-9a-f]{64}$ ]]; then fail 8 "${URL}.sha256 did not contain a SHA-256 digest. Refusing to install." fi if [[ "$EXPECTED" != "$ACTUAL" ]]; then fail 8 "CHECKSUM MISMATCH — nothing was installed. expected ${EXPECTED} got ${ACTUAL} The download does not match what the CDN says it published. Usually that is a truncated or cached-and-corrupted download, so trying again from a different network is worth doing. If it happens repeatedly, stop and report it." fi echo " OK ${ACTUAL}" echo "==> Unpacking" tar -xzf "${WORK}/${FILE}" -C "$WORK" \ || fail 9 "The archive downloaded and matched its checksum but would not unpack. Report this with the version number above." TOP="${WORK}/firecradle-agent-${VERSION}-linux-${ARCH}" INNER="${TOP}/build/installer/linux/install.sh" [[ -x "$INNER" ]] || [[ -f "$INNER" ]] \ || fail 9 "The archive does not contain build/installer/linux/install.sh, so there is nothing here that knows how to install it. Report this." echo "==> Installing" PREFIX="$PREFIX" bash "$INNER" \ || fail 10 "The installer ran and did not finish. The messages above are from it and say what stopped it; the system may be partly installed. Re-running this command is safe and is the usual repair." cat <<'DONE' Firecradle Agent is installed and the service is running. Next: open Firecradle from your applications menu and sign in. It will show a code and open your browser; that is the device sign-in, and it is what gives this device its own credential. Nothing was enrolled by this script. DONE