#!/bin/sh # Joins the box it runs on to the fleet. A person runs it from the line `./fleet invite ` prints, # a cloud box from cloud-init at first boot. Its arguments are the box's machine name in fleet-config.yaml # and a Tailscale key that works once and dies within the hour. Every box joins the tailnet under its # name with MagicDNS off through tailscaled-fleet, the fleet's own tailscaled with its own state and socket, # keeps root trusting the fleet's root keys (fleet-root-keys.timer), and gets Docker with live-restore and # Coolify's network on its fixed subnet. tailscaled-fleet runs in kernel mode with the stock tailscaled # masked, or, beside a Tailscale the box's owner is logged in to, in user-space mode, leaving that one alone. # `--control` adds Tailscale SSH, the fleet network, the firewall admitting only that network to the # metadata address, and Coolify at COOLIFY_VERSION, from that release's own files with auto-update off. # `--sync-root-keys` alone sets up the root keys, and `--move-to-tailscaled-fleet` moves the stock tailscaled's # node to tailscaled-fleet, on a box joined before either. Every step is idempotent. POSIX sh, since the # pasted line runs it as `sh` on a box with nothing installed yet. set -eu COOLIFY_VERSION=4.3.23 # Coolify's installer and upgrade script download compose files from Coolify's CDN, which serves only # the newest release. Both run here against the pinned release's files, mirrored in the CDN's layout. COOLIFY_RELEASE_URL=https://raw.githubusercontent.com/coollabsio/coolify/v$COOLIFY_VERSION COOLIFY_RELEASE_MIRROR=/data/coolify/release-mirror COOLIFY_CDN_LINE='CDN="https://cdn.coollabs.io/coolify"' FLEET_NETWORK=fleet FLEET_SUBNET=10.255.0.0/24 # Coolify's network, the one its Traefik publishes ports on, made here before Coolify would make it from # the pool: cloudflared on the host reaches Traefik through docker-proxy from this network's gateway, # the one address Traefik trusts forwarded headers from (fleetcli/apply/proxy.py). deepdish's, as found. ROOT_KEYS_SYNC=/usr/local/sbin/fleet-root-keys # The fleet's own tailscaled: its own unit, state and socket, which fleetcli/root_ssh.py names too. FLEET_TAILSCALE_UNIT=/etc/systemd/system/tailscaled-fleet.service FLEET_TAILSCALE_STATE=/var/lib/tailscale-fleet FLEET_TAILSCALE_SOCKET=/run/tailscale-fleet/tailscaled.sock # One past Tailscale's own 41641, so the fleet's never meets a Tailscale the box runs for itself. FLEET_TAILSCALE_PORT=41642 USERSPACE_FLAG=--tun=userspace-networking STOCK_TAILSCALE_UNIT=tailscaled.service STOCK_TAILSCALE_STATE=/var/lib/tailscale COOLIFY_NETWORK=coolify COOLIFY_SUBNET=10.0.1.0/24 COOLIFY_GATEWAY=10.0.1.1 METADATA_ADDRESS=169.254.169.254 # Where Docker takes a new network's subnet from: outside the fleet VPC (10.20.0.0/16), FLEET_SUBNET, # COOLIFY_SUBNET and the tailnet (100.64.0.0/10). Coolify's installer keeps a daemon.json that names a pool. DOCKER_ADDRESS_POOL=172.16.0.0/12 DOCKER_PACKAGES="docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" fail() { echo "join.sh: $1" >&2 exit 1 } apt_install() { for package in "$@"; do if ! dpkg -s "$package" >/dev/null 2>&1; then apt-get update -q apt-get install -y -q "$@" return fi done } write_daemon_json() { mkdir -p /etc/docker [ -f /etc/docker/daemon.json ] || echo '{}' >/etc/docker/daemon.json jq --arg pool "$DOCKER_ADDRESS_POOL" '{ "log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}, "default-address-pools": [{"base": $pool, "size": 24}] } * . | .["live-restore"] = true' /etc/docker/daemon.json >/etc/docker/daemon.json.join mv /etc/docker/daemon.json.join /etc/docker/daemon.json } add_docker_repository() { os=$(sed -n 's/^ID=//p' /etc/os-release) codename=$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release) case $os in ubuntu | debian) ;; *) fail "Docker's apt repository covers Ubuntu and Debian; this box is $os" ;; esac install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/$os/gpg" -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc cat >/etc/apt/sources.list.d/docker.sources <"$ROOT_KEYS_SYNC" <<'EOF' #!/bin/sh # Makes root's fleet-root lines in authorized_keys the keys at ROOT_KEYS_URL, which ./fleet root-key writes: # the current key and, once it has rotated, the one before it. Each may log in from the tailnet and the box # itself alone, so a key that leaks opens nothing from the internet. A fetch that fails, or a list that is # not one or two of the fleet's ed25519 keys, changes nothing. Written by join.sh; fleet-root-keys.timer # runs it at boot and every five minutes. set -eu ROOT_KEYS_URL=https://fleet-root-keys-databob-labs.s3.us-east-2.amazonaws.com/fleet-root-keys.pub FROM=100.64.0.0/10,127.0.0.1,::1 AUTHORIZED=/root/.ssh/authorized_keys refuse() { echo "fleet-root-keys: $1; root's keys are unchanged" >&2 exit 1 } keys=$(curl -fsSL --max-time 30 "$ROOT_KEYS_URL") || refuse "cannot fetch $ROOT_KEYS_URL" count=$(printf '%s\n' "$keys" | grep -c . || true) if [ "$count" -lt 1 ] || [ "$count" -gt 2 ]; then refuse "$ROOT_KEYS_URL holds $count keys, not one or two" fi if printf '%s\n' "$keys" | grep -Evxq 'ssh-ed25519 [A-Za-z0-9+/]+={0,2} fleet-root'; then refuse "$ROOT_KEYS_URL holds a line that is not an ssh-ed25519 key named fleet-root" fi install -d -m 700 /root/.ssh touch "$AUTHORIZED" { grep -v ' fleet-root$' "$AUTHORIZED" || true printf '%s\n' "$keys" | sed "s|^|from=\"$FROM\" |" } >"$AUTHORIZED.fleet" chmod 600 "$AUTHORIZED.fleet" if cmp -s "$AUTHORIZED.fleet" "$AUTHORIZED"; then rm "$AUTHORIZED.fleet" else mv "$AUTHORIZED.fleet" "$AUTHORIZED" fi EOF chmod 755 "$ROOT_KEYS_SYNC" cat >/etc/systemd/system/fleet-root-keys.service </etc/systemd/system/fleet-root-keys.timer </dev/null || return 1 state=$(tailscale status --json 2>/dev/null | jq -r .BackendState) [ "$state" != NeedsLogin ] && [ "$state" != NoState ] } # The flag tailscaled-fleet adds to its own state, socket and port, with a leading space, or none: user-space # beside the owner's Tailscale, since two in kernel mode fight over one box's routes; kernel mode alone, so the # box's own programs reach the tailnet. A box set up before keeps the mode it has. fleet_tailscale_mode() { if [ -f "$FLEET_TAILSCALE_UNIT" ]; then if grep -q -- "$USERSPACE_FLAG" "$FLEET_TAILSCALE_UNIT"; then echo " $USERSPACE_FLAG"; fi elif own_tailscale_taken; then echo " $USERSPACE_FLAG" fi } # Tailscale's binaries with the stock tailscaled masked, so no package upgrade starts it beside the fleet's. mask_stock_tailscale() { if ! command -v tailscale >/dev/null; then curl -fsSL https://tailscale.com/install.sh | sh fi systemctl mask --now "$STOCK_TAILSCALE_UNIT" } install_fleet_tailscale() { mode=$1 # No `tailscaled --cleanup` around it, as tailscaled.service has: beside the owner's Tailscale it would take # down that one's routes and firewall rules, and alone tailscaled replaces its own when it starts. cat >"$FLEET_TAILSCALE_UNIT" </dev/null | jq -r '.Self.Tags // [] | join(",")') case ",$tags," in *,tag:box,* | *,tag:control,*) ;; *) fail "the stock tailscaled is not this box on the fleet's tailnet; nothing was changed" ;; esac [ -f "$STOCK_TAILSCALE_STATE/tailscaled.state" ] || fail "no $STOCK_TAILSCALE_STATE/tailscaled.state to move" systemctl mask --now "$STOCK_TAILSCALE_UNIT" mkdir -p "$FLEET_TAILSCALE_STATE" cp -a "$STOCK_TAILSCALE_STATE/." "$FLEET_TAILSCALE_STATE/" install_fleet_tailscale "" fleet_tailscale status --self --peers=false } create_network() { name=$1 shift if docker network inspect "$name" >/dev/null 2>&1; then return fi docker network create "$name" "$@" } install_firewall() { apt_install nftables mkdir -p /etc/fleet cat >/etc/fleet/firewall.nft </etc/systemd/system/fleet-firewall.service </dev/null; then systemctl reload docker else add_docker_repository # shellcheck disable=SC2086 apt_install $DOCKER_PACKAGES fi create_network "$COOLIFY_NETWORK" --attachable --subnet "$COOLIFY_SUBNET" --gateway "$COOLIFY_GATEWAY" if [ "$control" = true ]; then create_network "$FLEET_NETWORK" --subnet "$FLEET_SUBNET" install_firewall install_coolify fi fleet_tailscale status --self --peers=false