diff --git a/CHANGELOG.md b/CHANGELOG.md index 37db79886..2911078dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixed - Made coordinatorless generic provider live smokes skip coordinator-only history and always clean up acquired leases after later lifecycle failures. +- Replaced privileged managed Linux Code Server and Tailscale installer scripts with checksum-verified archives or Tailscale's signed package repository with a pinned keyring in both CLI and coordinator bootstrap paths. Thanks @TurboTheTurtle. ## 0.35.0 - 2026-07-04 diff --git a/docs/features/runner-bootstrap.md b/docs/features/runner-bootstrap.md index 896372977..c0f887771 100644 --- a/docs/features/runner-bootstrap.md +++ b/docs/features/runner-bootstrap.md @@ -98,10 +98,12 @@ and built artifacts out of the image. See ## Tailscale -`--tailscale` on a managed Linux lease is also optional. Bootstrap installs the -Tailscale package, brings the box up on the configured tailnet, writes non-secret -metadata under `/var/lib/crabbox` (such as `tailscale-ipv4`, `tailscale-hostname`, -and exit-node details), and extends `crabbox-ready` with a bounded check that a +`--tailscale` on a managed Linux lease is also optional. The default package +mode installs Tailscale from its signed APT repository with a pinned keyring; +the opt-in pinned mode verifies a versioned static archive by SHA-256. Bootstrap +then brings the box up on the configured tailnet, writes non-secret metadata +under `/var/lib/crabbox` (such as `tailscale-ipv4`, `tailscale-hostname`, and +exit-node details), and extends `crabbox-ready` with a bounded check that a `100.x` address has appeared. The auth key is piped to `tailscale up` through stdin and is not persisted or diff --git a/docs/features/tailscale.md b/docs/features/tailscale.md index 2ea6da09f..f0651d3ea 100644 --- a/docs/features/tailscale.md +++ b/docs/features/tailscale.md @@ -192,11 +192,13 @@ The auth key is never stored in lease records, provider labels, run logs, or loc config. The short-lived key can still appear in user-data at the provider, so the Worker only mints one-off ephemeral keys — never long-lived reusable keys. -The default installer mode runs Tailscale's package install script. Set -`CRABBOX_TAILSCALE_INSTALL_MODE=pinned` to download a static Tailscale archive, -verify the configured SHA-256 checksum, install the `tailscale` and `tailscaled` -binaries, and record the client version. The built-in pinned defaults track the -Islo Tailscale build so both bootstrap paths use the same binary version. +The default `package` mode configures Tailscale's signed stable APT repository, +verifies and scopes its pinned keyring with `signed-by`, installs the current +package, and records the client version. Set +`CRABBOX_TAILSCALE_INSTALL_MODE=pinned` to download a static +archive, verify the configured SHA-256 checksum, and install the `tailscale` and +`tailscaled` binaries. Neither mode pipes a downloaded installer script into a +root shell. On release, `crabbox stop` attempts a best-effort remote `tailscale logout` before provider cleanup when SSH is still reachable. Coordinator cleanup does not delete a diff --git a/docs/infrastructure.md b/docs/infrastructure.md index 9f73a92d4..c2dbf343c 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -260,10 +260,10 @@ CRABBOX_TAILSCALE_CLIENT_ID CRABBOX_TAILSCALE_CLIENT_SECRET CRABBOX_TAILSCALE_TAILNET=- # or an explicit tailnet/org CRABBOX_TAILSCALE_TAGS=tag:crabbox # requested-tag allowlist/default -CRABBOX_TAILSCALE_INSTALL_MODE=package # or pinned -CRABBOX_TAILSCALE_VERSION=1.98.4 # pinned mode -CRABBOX_TAILSCALE_SHA256_AMD64=... # pinned mode -CRABBOX_TAILSCALE_SHA256_ARM64=... # pinned mode +CRABBOX_TAILSCALE_INSTALL_MODE=package # package (default) or pinned static archive +CRABBOX_TAILSCALE_VERSION=1.98.4 # static archive version +CRABBOX_TAILSCALE_SHA256_AMD64=... # amd64 archive checksum +CRABBOX_TAILSCALE_SHA256_ARM64=... # arm64 archive checksum ``` For one tag, assign the same tag to the OAuth client. For multiple tags, either diff --git a/docs/operations.md b/docs/operations.md index 25f4eb742..12c4569d7 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -394,7 +394,7 @@ CRABBOX_TAILSCALE_CLIENT_SECRET required for brokered --tailscale CRABBOX_TAILSCALE_TAILNET optional CRABBOX_TAILSCALE_TAGS optional CRABBOX_TAILSCALE_ENABLED optional; set 0 to disable brokered Tailscale -CRABBOX_TAILSCALE_INSTALL_MODE optional; package or pinned +CRABBOX_TAILSCALE_INSTALL_MODE optional; package (default) or pinned static archive CRABBOX_TAILSCALE_VERSION optional pinned static build version CRABBOX_TAILSCALE_SHA256_AMD64 optional pinned amd64 archive checksum CRABBOX_TAILSCALE_SHA256_ARM64 optional pinned arm64 archive checksum diff --git a/internal/cli/bootstrap.go b/internal/cli/bootstrap.go index 486c229ff..01e72a0a4 100644 --- a/internal/cli/bootstrap.go +++ b/internal/cli/bootstrap.go @@ -19,6 +19,10 @@ const ( defaultTailscaleVersion = "1.98.4" defaultTailscaleAMD64SHA256 = "e6c08a8ee7e63e69aaf1b62ecd12672b3883fbcd2a176bf6cfa42a15fdce0b6b" defaultTailscaleARM64SHA256 = "3cb068eb1368b6bb218d0ef0aa0a7a679a7156b7c979e2279cc2c2321b5f05c7" + defaultTailscaleKeyringSHA256 = "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39" + defaultCodeServerVersion = "4.126.0" + defaultCodeServerAMD64SHA256 = "54b648d010c02b6583aa06bd8d2aaf109fc624479b9bc2ff71cb94807ac39afa" + defaultCodeServerARM64SHA256 = "441614708ae81b13f14b26db41da8f46f88d7d092c08343a42a0c6c52c51a69d" googleLinuxSigningKeyFingerprint = "EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796" ) @@ -1455,12 +1459,37 @@ chmod 0755 /usr/local/bin/crabbox-configure-desktop-theme } if cfg.Code { parts = append(parts, ` retry apt-get install -y --no-install-recommends libatomic1 - retry env HOME=/root sh -c 'curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/usr/local' + `+cloudInitCodeServerInstallBootstrap()+` /usr/local/bin/code-server --version >/dev/null`) } return strings.Join(parts, "\n") } +func cloudInitCodeServerInstallBootstrap() string { + return `CS_VERSION=` + shellQuote(defaultCodeServerVersion) + ` + case "$(uname -m)" in + x86_64) CS_ARCH=amd64; CS_SHA256=` + shellQuote(defaultCodeServerAMD64SHA256) + ` ;; + aarch64|arm64) CS_ARCH=arm64; CS_SHA256=` + shellQuote(defaultCodeServerARM64SHA256) + ` ;; + *) echo "unsupported code-server architecture: $(uname -m)" >&2; exit 3 ;; + esac + if [ -z "$CS_SHA256" ]; then echo "missing code-server checksum for $CS_ARCH" >&2; exit 3; fi + CS_INSTALL_DIR="$(mktemp -d)" + trap 'rm -rf "$CS_INSTALL_DIR"' EXIT + CS_ARCHIVE="$CS_INSTALL_DIR/code-server.tgz" + retry sh -c "curl -fsSL -o \"$CS_ARCHIVE\" \"https://github.com/coder/code-server/releases/download/v${CS_VERSION}/code-server-${CS_VERSION}-linux-${CS_ARCH}.tar.gz\"" + printf '%s %s\n' "$CS_SHA256" "$CS_ARCHIVE" | sha256sum -c - + tar -xzf "$CS_ARCHIVE" -C "$CS_INSTALL_DIR" --strip-components=1 + rm -f "$CS_ARCHIVE" + rm -rf /usr/local/lib/code-server + install -d -m 0755 /usr/local/lib/code-server + cp -a "$CS_INSTALL_DIR/." /usr/local/lib/code-server/ + # cp -a preserves mktemp's private root mode; restore traversal for lease users. + chmod 0755 /usr/local/lib/code-server + ln -sfn /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server + rm -rf "$CS_INSTALL_DIR" + trap - EXIT` +} + func indentCloudInitRuncmd(script string) string { if script == "" { return "" @@ -1651,8 +1680,8 @@ func cloudInitTailscaleBootstrap(cfg Config) string { } func cloudInitTailscaleInstallBootstrap() string { - if strings.TrimSpace(os.Getenv("CRABBOX_TAILSCALE_INSTALL_MODE")) != "pinned" { - return "retry sh -c 'curl -fsSL https://tailscale.com/install.sh | sh'" + if !strings.EqualFold(strings.TrimSpace(os.Getenv("CRABBOX_TAILSCALE_INSTALL_MODE")), "pinned") { + return cloudInitTailscalePackageInstallBootstrap() } version := firstNonEmpty(strings.TrimSpace(os.Getenv("CRABBOX_TAILSCALE_VERSION")), defaultTailscaleVersion) amd64SHA := firstNonEmpty(strings.TrimSpace(os.Getenv("CRABBOX_TAILSCALE_SHA256_AMD64")), defaultTailscaleAMD64SHA256) @@ -1663,6 +1692,7 @@ func cloudInitTailscaleInstallBootstrap() string { aarch64|arm64) TS_ARCH=arm64; TS_SHA256=` + shellQuote(arm64SHA) + ` ;; *) echo "unsupported Tailscale architecture: $(uname -m)" >&2; exit 3 ;; esac + if [ -z "$TS_SHA256" ]; then echo "missing Tailscale checksum for $TS_ARCH" >&2; exit 3; fi TS_INSTALL_DIR="$(mktemp -d)" trap 'rm -rf "$TS_INSTALL_DIR"' EXIT TS_ARCHIVE="$TS_INSTALL_DIR/tailscale.tgz" @@ -1687,9 +1717,39 @@ func cloudInitTailscaleInstallBootstrap() string { printf '%s\n' '[Install]' printf '%s\n' 'WantedBy=multi-user.target' } >/etc/systemd/system/tailscaled.service + rm -rf "$TS_INSTALL_DIR" + trap - EXIT systemctl daemon-reload || true` } +func cloudInitTailscalePackageInstallBootstrap() string { + return `if [ ! -r /etc/os-release ]; then + echo "Tailscale package install requires /etc/os-release" >&2 + exit 3 + fi + . /etc/os-release + TS_DIST_ID="${ID:-}" + TS_CODENAME="${VERSION_CODENAME:-}" + case "$TS_DIST_ID" in + ubuntu|debian) ;; + *) echo "unsupported Tailscale package distribution: $TS_DIST_ID" >&2; exit 3 ;; + esac + case "$TS_CODENAME" in + ''|*[!a-z0-9.-]*) echo "invalid Tailscale package codename: $TS_CODENAME" >&2; exit 3 ;; + esac + install -d -m 0755 /usr/share/keyrings + TS_KEYRING_TMP="$(mktemp)" + trap 'rm -f "$TS_KEYRING_TMP"' EXIT + retry curl -fsSL -o "$TS_KEYRING_TMP" "https://pkgs.tailscale.com/stable/${TS_DIST_ID}/${TS_CODENAME}.noarmor.gpg" + printf '%s %s\n' '` + defaultTailscaleKeyringSHA256 + `' "$TS_KEYRING_TMP" | sha256sum -c - + install -m 0644 "$TS_KEYRING_TMP" /usr/share/keyrings/tailscale-archive-keyring.gpg + printf 'deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/%s %s main\n' "$TS_DIST_ID" "$TS_CODENAME" >/etc/apt/sources.list.d/tailscale.list + rm -f "$TS_KEYRING_TMP" + trap - EXIT + retry apt-get update + retry apt-get install -y --no-install-recommends tailscale` +} + // cloudInitPondHostsBootstrap installs /usr/local/bin/crabbox-pond-hosts and a // systemd timer that rewrites /etc/hosts.cbx plus a managed /etc/hosts block // every 30s with one entry per pond peer reachable on the local tailnet. Peers diff --git a/internal/cli/bootstrap_test.go b/internal/cli/bootstrap_test.go index 052585134..423866b9a 100644 --- a/internal/cli/bootstrap_test.go +++ b/internal/cli/bootstrap_test.go @@ -404,9 +404,15 @@ func TestCloudInitCodeProfile(t *testing.T) { cfg.Code = true got := cloudInit(cfg, "ssh-ed25519 test") for _, want := range []string{ - "https://code-server.dev/install.sh", - "env HOME=/root", - "--method=standalone --prefix=/usr/local", + "CS_VERSION='4.126.0'", + "x86_64) CS_ARCH=amd64; CS_SHA256='54b648d010c02b6583aa06bd8d2aaf109fc624479b9bc2ff71cb94807ac39afa'", + "aarch64|arm64) CS_ARCH=arm64; CS_SHA256='441614708ae81b13f14b26db41da8f46f88d7d092c08343a42a0c6c52c51a69d'", + "https://github.com/coder/code-server/releases/download/v${CS_VERSION}/code-server-${CS_VERSION}-linux-${CS_ARCH}.tar.gz", + "sha256sum -c -", + "/usr/local/lib/code-server", + "chmod 0755 /usr/local/lib/code-server", + `rm -rf "$CS_INSTALL_DIR"`, + "trap - EXIT", "/usr/local/bin/code-server --version >/dev/null", "test -x /usr/local/bin/code-server", } { @@ -414,6 +420,16 @@ func TestCloudInitCodeProfile(t *testing.T) { t.Fatalf("cloudInit(code) missing %q", want) } } + copyIndex := strings.Index(got, `cp -a "$CS_INSTALL_DIR/." /usr/local/lib/code-server/`) + archiveCleanupIndex := strings.Index(got, `rm -f "$CS_ARCHIVE"`) + chmodIndex := strings.Index(got, "chmod 0755 /usr/local/lib/code-server") + linkIndex := strings.Index(got, "ln -sfn /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server") + if archiveCleanupIndex < 0 || copyIndex <= archiveCleanupIndex || chmodIndex <= copyIndex || linkIndex <= chmodIndex { + t.Fatal("cloudInit(code) must restore install-root traversal after copying and before exposing the binary") + } + if strings.Contains(got, "https://code-server.dev/install.sh") || strings.Contains(got, "curl -fsSL https://code-server.dev/install.sh | sh") { + t.Fatal("cloudInit(code) must not pipe the code-server installer script to root shell") + } if strings.Contains(cloudInit(baseConfig(), "ssh-ed25519 test"), "code-server") { t.Fatal("cloudInit should not install code-server by default") } @@ -430,7 +446,10 @@ func TestCloudInitTailscaleProfile(t *testing.T) { cfg.Tailscale.ExitNodeAllowLANAccess = true got := cloudInit(cfg, "ssh-ed25519 test") for _, want := range []string{ - "https://tailscale.com/install.sh", + "https://pkgs.tailscale.com/stable/${TS_DIST_ID}/${TS_CODENAME}.noarmor.gpg", + "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39", + "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/%s %s main", + "retry apt-get install -y --no-install-recommends tailscale", "install -d -m 0750 -o 'runner' -g 'runner' /var/lib/crabbox", "printf '%s' \"$TS_AUTHKEY\" | tailscale up --auth-key=file:/dev/stdin --hostname='crabbox-blue-lobster' --advertise-tags='tag:crabbox' --exit-node='mac-studio.tailnet.ts.net' --exit-node-allow-lan-access", "printf '%s\\n' 'crabbox-blue-lobster' > /var/lib/crabbox/tailscale-hostname", @@ -455,13 +474,19 @@ func TestCloudInitTailscaleProfile(t *testing.T) { if strings.Contains(got, "tailscale logout") || strings.Contains(got, "WantedBy=halt.target reboot.target shutdown.target") { t.Fatal("cloudInit(tailscale) must not install a normal-reboot logout hook") } + if strings.Contains(got, "https://tailscale.com/install.sh") { + t.Fatal("cloudInit(tailscale) must not pipe the Tailscale installer script to root shell") + } + if strings.Contains(got, "tailscale_${TS_VERSION}_${TS_ARCH}.tgz") { + t.Fatal("default Tailscale package mode must not use the pinned archive path") + } if strings.Contains(cloudInit(baseConfig(), "ssh-ed25519 test"), "tailscale up") { t.Fatal("cloudInit should not install Tailscale by default") } } func TestCloudInitTailscalePinnedStaticInstall(t *testing.T) { - t.Setenv("CRABBOX_TAILSCALE_INSTALL_MODE", "pinned") + t.Setenv("CRABBOX_TAILSCALE_INSTALL_MODE", " Pinned ") t.Setenv("CRABBOX_TAILSCALE_VERSION", "1.98.4") t.Setenv("CRABBOX_TAILSCALE_SHA256_AMD64", "amd64sum") t.Setenv("CRABBOX_TAILSCALE_SHA256_ARM64", "arm64sum") @@ -486,6 +511,9 @@ func TestCloudInitTailscalePinnedStaticInstall(t *testing.T) { if strings.Contains(got, "https://tailscale.com/install.sh") { t.Fatal("pinned Tailscale install should not use package install script") } + if strings.Contains(got, "tailscale-archive-keyring") { + t.Fatal("case-insensitive pinned Tailscale mode should not use the package repository") + } } // TestCloudInitTailscaleHonorsControlURL exercises the self-hosted control diff --git a/worker/src/bootstrap.ts b/worker/src/bootstrap.ts index 2ec6ed3bb..ac02d7027 100644 --- a/worker/src/bootstrap.ts +++ b/worker/src/bootstrap.ts @@ -13,6 +13,13 @@ const ubuntuWSLRootFSURL = "https://cloud-images.ubuntu.com/wsl/releases/24.04/20240423/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz"; const ubuntuWSLRootFSSHA256 = "8251e27ffff381a4af5f41dcb94d867de3e0d9774a9241908ab34555d99315ea"; const googleLinuxSigningKeyFingerprint = "EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796"; +const defaultCodeServerVersion = "4.126.0"; +const defaultCodeServerSHA256 = { + amd64: "54b648d010c02b6583aa06bd8d2aaf109fc624479b9bc2ff71cb94807ac39afa", + arm64: "441614708ae81b13f14b26db41da8f46f88d7d092c08343a42a0c6c52c51a69d", +} as const; +const defaultTailscaleKeyringSHA256 = + "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39"; export function awsUserData(config: LeaseConfig): string { if (config.target === "windows") { @@ -1475,12 +1482,39 @@ ${themeConfigure} systemctl daemon-reload } if (config.code) { parts.push(` retry apt-get install -y --no-install-recommends libatomic1 - retry env HOME=/root sh -c 'curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/usr/local' + ${codeServerInstallBootstrap()} /usr/local/bin/code-server --version >/dev/null`); } return parts.join("\n"); } +function codeServerInstallBootstrap(): string { + const version = shellQuote(defaultCodeServerVersion); + const amd64SHA = shellQuote(defaultCodeServerSHA256.amd64); + const arm64SHA = shellQuote(defaultCodeServerSHA256.arm64); + return `CS_VERSION=${version} + case "$(uname -m)" in + x86_64) CS_ARCH=amd64; CS_SHA256=${amd64SHA} ;; + aarch64|arm64) CS_ARCH=arm64; CS_SHA256=${arm64SHA} ;; + *) echo "unsupported code-server architecture: $(uname -m)" >&2; exit 3 ;; + esac + if [ -z "$CS_SHA256" ]; then echo "missing code-server checksum for $CS_ARCH" >&2; exit 3; fi + CS_INSTALL_DIR="$(mktemp -d)" + trap 'rm -rf "$CS_INSTALL_DIR"' EXIT + CS_ARCHIVE="$CS_INSTALL_DIR/code-server.tgz" + retry sh -c "curl -fsSL -o \\"$CS_ARCHIVE\\" \\"https://github.com/coder/code-server/releases/download/v\${CS_VERSION}/code-server-\${CS_VERSION}-linux-\${CS_ARCH}.tar.gz\\"" + printf '%s %s\\n' "$CS_SHA256" "$CS_ARCHIVE" | sha256sum -c - + tar -xzf "$CS_ARCHIVE" -C "$CS_INSTALL_DIR" --strip-components=1 + rm -f "$CS_ARCHIVE" + rm -rf /usr/local/lib/code-server + install -d -m 0755 /usr/local/lib/code-server + cp -a "$CS_INSTALL_DIR/." /usr/local/lib/code-server/ + chmod 0755 /usr/local/lib/code-server + ln -sfn /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server + rm -rf "$CS_INSTALL_DIR" + trap - EXIT`; +} + function indentRuncmdScript(script: string): string { if (script.length === 0) { return ""; @@ -1498,7 +1532,7 @@ function indentRuncmdScript(script: string): string { function tailscaleInstallBootstrap(config: LeaseConfig): string { if (config.tailscaleInstallMode !== "pinned") { - return "retry sh -c 'curl -fsSL https://tailscale.com/install.sh | sh'"; + return tailscalePackageInstallBootstrap(); } const version = shellQuote(config.tailscaleVersion || "1.98.4"); const amd64SHA = shellQuote(config.tailscaleSHA256?.amd64 || ""); @@ -1509,6 +1543,7 @@ function tailscaleInstallBootstrap(config: LeaseConfig): string { aarch64|arm64) TS_ARCH=arm64; TS_SHA256=${arm64SHA} ;; *) echo "unsupported Tailscale architecture: $(uname -m)" >&2; exit 3 ;; esac + if [ -z "$TS_SHA256" ]; then echo "missing Tailscale checksum for $TS_ARCH" >&2; exit 3; fi TS_INSTALL_DIR="$(mktemp -d)" trap 'rm -rf "$TS_INSTALL_DIR"' EXIT TS_ARCHIVE="$TS_INSTALL_DIR/tailscale.tgz" @@ -1517,6 +1552,8 @@ function tailscaleInstallBootstrap(config: LeaseConfig): string { tar -xzf "$TS_ARCHIVE" -C "$TS_INSTALL_DIR" --strip-components=1 install -m 0755 "$TS_INSTALL_DIR/tailscale" /usr/local/bin/tailscale install -m 0755 "$TS_INSTALL_DIR/tailscaled" /usr/local/sbin/tailscaled + rm -rf "$TS_INSTALL_DIR" + trap - EXIT install -d -m 0755 /var/lib/tailscale /run/tailscale { printf '%s\\n' '[Unit]' @@ -1536,6 +1573,34 @@ function tailscaleInstallBootstrap(config: LeaseConfig): string { systemctl daemon-reload || true`; } +function tailscalePackageInstallBootstrap(): string { + return `if [ ! -r /etc/os-release ]; then + echo "Tailscale package install requires /etc/os-release" >&2 + exit 3 + fi + . /etc/os-release + TS_DIST_ID="\${ID:-}" + TS_CODENAME="\${VERSION_CODENAME:-}" + case "$TS_DIST_ID" in + ubuntu|debian) ;; + *) echo "unsupported Tailscale package distribution: $TS_DIST_ID" >&2; exit 3 ;; + esac + case "$TS_CODENAME" in + ''|*[!a-z0-9.-]*) echo "invalid Tailscale package codename: $TS_CODENAME" >&2; exit 3 ;; + esac + install -d -m 0755 /usr/share/keyrings + TS_KEYRING_TMP="$(mktemp)" + trap 'rm -f "$TS_KEYRING_TMP"' EXIT + retry curl -fsSL -o "$TS_KEYRING_TMP" "https://pkgs.tailscale.com/stable/\${TS_DIST_ID}/\${TS_CODENAME}.noarmor.gpg" + printf '%s %s\\n' '${defaultTailscaleKeyringSHA256}' "$TS_KEYRING_TMP" | sha256sum -c - + install -m 0644 "$TS_KEYRING_TMP" /usr/share/keyrings/tailscale-archive-keyring.gpg + printf 'deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/%s %s main\\n' "$TS_DIST_ID" "$TS_CODENAME" >/etc/apt/sources.list.d/tailscale.list + rm -f "$TS_KEYRING_TMP" + trap - EXIT + retry apt-get update + retry apt-get install -y --no-install-recommends tailscale`; +} + function tailscaleBootstrap(config: LeaseConfig): string { if (!config.tailscaleAuthKey) { return ` echo "tailscale requested but no auth key was injected" >&2 diff --git a/worker/src/tailscale.ts b/worker/src/tailscale.ts index 08b218856..4aa2fd28a 100644 --- a/worker/src/tailscale.ts +++ b/worker/src/tailscale.ts @@ -70,9 +70,8 @@ export function tailscaleDefaultTags(env: Env): string[] { export function tailscaleInstallConfig(env: Env): TailscaleInstallConfig { const requested = (env.CRABBOX_TAILSCALE_INSTALL_MODE ?? "package").trim().toLowerCase(); - const mode = requested === "pinned" ? "pinned" : "package"; return { - mode, + mode: requested === "pinned" ? "pinned" : "package", version: env.CRABBOX_TAILSCALE_VERSION?.trim() || defaultPinnedTailscaleVersion, sha256: { amd64: env.CRABBOX_TAILSCALE_SHA256_AMD64?.trim() || defaultPinnedTailscaleSHA256.amd64, diff --git a/worker/test/bootstrap.test.ts b/worker/test/bootstrap.test.ts index c7c8dde88..f1ddba45b 100644 --- a/worker/test/bootstrap.test.ts +++ b/worker/test/bootstrap.test.ts @@ -413,11 +413,55 @@ describe("cloud-init bootstrap", () => { const plain = cloudInit(config); expect(plain).not.toContain("code-server"); const got = cloudInit({ ...config, code: true }); - expect(got).toContain("https://code-server.dev/install.sh"); - expect(got).toContain("env HOME=/root"); - expect(got).toContain("--method=standalone --prefix=/usr/local"); + expect(got).not.toContain("https://code-server.dev/install.sh"); + expect(got).not.toContain("curl -fsSL https://code-server.dev/install.sh | sh"); + expect(got).toContain("CS_VERSION='4.126.0'"); + expect(got).toContain( + "x86_64) CS_ARCH=amd64; CS_SHA256='54b648d010c02b6583aa06bd8d2aaf109fc624479b9bc2ff71cb94807ac39afa'", + ); + expect(got).toContain( + "aarch64|arm64) CS_ARCH=arm64; CS_SHA256='441614708ae81b13f14b26db41da8f46f88d7d092c08343a42a0c6c52c51a69d'", + ); + expect(got).toContain( + "https://github.com/coder/code-server/releases/download/v${CS_VERSION}/code-server-${CS_VERSION}-linux-${CS_ARCH}.tar.gz", + ); + expect(got).toContain("sha256sum -c -"); + expect(got).toContain("/usr/local/lib/code-server"); + expect(got).toContain("chmod 0755 /usr/local/lib/code-server"); expect(got).toContain("/usr/local/bin/code-server --version >/dev/null"); expect(got).toContain("test -x /usr/local/bin/code-server"); + const copyIndex = got.indexOf('cp -a "$CS_INSTALL_DIR/." /usr/local/lib/code-server/'); + const archiveCleanupIndex = got.indexOf('rm -f "$CS_ARCHIVE"'); + const chmodIndex = got.indexOf("chmod 0755 /usr/local/lib/code-server"); + const linkIndex = got.indexOf( + "ln -sfn /usr/local/lib/code-server/bin/code-server /usr/local/bin/code-server", + ); + expect(copyIndex).toBeGreaterThanOrEqual(0); + expect(archiveCleanupIndex).toBeGreaterThanOrEqual(0); + expect(copyIndex).toBeGreaterThan(archiveCleanupIndex); + expect(chmodIndex).toBeGreaterThan(copyIndex); + expect(linkIndex).toBeGreaterThan(chmodIndex); + }); + + it("cleans pinned install directories when code-server and Tailscale are combined", () => { + const got = cloudInit({ + ...config, + code: true, + tailscale: true, + tailscaleHostname: "crabbox-blue-lobster", + tailscaleAuthKey: "tskey-secret", + tailscaleInstallMode: "pinned", + }); + + const tailscaleTrap = got.indexOf(`trap 'rm -rf "$TS_INSTALL_DIR"' EXIT`); + const tailscaleCleanup = got.indexOf(`rm -rf "$TS_INSTALL_DIR"\n trap - EXIT`); + const codeServerTrap = got.indexOf(`trap 'rm -rf "$CS_INSTALL_DIR"' EXIT`); + const codeServerCleanup = got.indexOf(`rm -rf "$CS_INSTALL_DIR"\n trap - EXIT`); + + expect(tailscaleTrap).toBeGreaterThanOrEqual(0); + expect(tailscaleCleanup).toBeGreaterThan(tailscaleTrap); + expect(codeServerTrap).toBeGreaterThan(tailscaleCleanup); + expect(codeServerCleanup).toBeGreaterThan(codeServerTrap); }); it("adds Tailscale setup only when requested", () => { @@ -433,7 +477,16 @@ describe("cloud-init bootstrap", () => { tailscaleExitNode: "mac-studio.tailnet.ts.net", tailscaleExitNodeAllowLanAccess: true, }); - expect(got).toContain("https://tailscale.com/install.sh"); + expect(got).not.toContain("https://tailscale.com/install.sh"); + expect(got).toContain( + "https://pkgs.tailscale.com/stable/${TS_DIST_ID}/${TS_CODENAME}.noarmor.gpg", + ); + expect(got).toContain("3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39"); + expect(got).toContain( + "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/%s %s main", + ); + expect(got).toContain("retry apt-get install -y --no-install-recommends tailscale"); + expect(got).not.toContain("tailscale_${TS_VERSION}_${TS_ARCH}.tgz"); expect(got).toContain("systemctl disable crabbox-tailscale-logout.service"); expect(got).not.toContain("tailscale logout"); expect(got).not.toContain("WantedBy=halt.target reboot.target shutdown.target"); diff --git a/worker/test/tailscale.test.ts b/worker/test/tailscale.test.ts index df26649be..eefc0cc38 100644 --- a/worker/test/tailscale.test.ts +++ b/worker/test/tailscale.test.ts @@ -183,4 +183,13 @@ describe("tailscale preflight", () => { sha256: { amd64: "amd", arm64: "arm" }, }); }); + + it("preserves package installer mode and defaults to it", () => { + expect( + tailscaleInstallConfig({ + CRABBOX_TAILSCALE_INSTALL_MODE: "package", + }).mode, + ).toBe("package"); + expect(tailscaleInstallConfig({}).mode).toBe("package"); + }); });