|
| 1 | +defmodule Mix.Tasks.Firecracker.Install do |
| 2 | + @shortdoc "Download, verify, and install the pinned Firecracker release" |
| 3 | + @moduledoc """ |
| 4 | + Downloads, verifies, and installs the pinned Firecracker release (v1.16.0) |
| 5 | + for the current CPU architecture. |
| 6 | +
|
| 7 | + mix firecracker.install [--prefix DIR] |
| 8 | +
|
| 9 | + Steps performed: |
| 10 | +
|
| 11 | + 1. Detects the CPU architecture (`x86_64` or `aarch64`). |
| 12 | + 2. Downloads the release tarball and verifies its SHA-256 checksum. |
| 13 | + 3. Extracts the tarball, then copies the binaries to `<prefix>/firecracker` |
| 14 | + and `<prefix>/jailer` using the **bare basenames** `firecracker` and |
| 15 | + `jailer`. The setuid helper validates binaries via `SafeBin<"firecracker">` |
| 16 | + and `SafeBin<"jailer">`, which match on basename only — version-stamped |
| 17 | + names such as `firecracker-v1.16.0-x86_64` are rejected unconditionally. |
| 18 | + 4. Marks both binaries executable (`0o755`). |
| 19 | + 5. Prints the `/etc/hyper/config.toml` snippet the operator needs to paste. |
| 20 | +
|
| 21 | + This task installs **unprivileged** binaries and prints configuration. |
| 22 | + Privilege at runtime is handled by `hyper-suidhelper` (the setuid helper). |
| 23 | + This task does **not** setuid `firecracker` or `jailer`. Install and setuid |
| 24 | + the helper separately with `mix suidhelper.install`. |
| 25 | +
|
| 26 | + ## Options |
| 27 | +
|
| 28 | + * `--prefix DIR` — installation directory (default: `/opt/firecracker`). |
| 29 | +
|
| 30 | + ## Security requirements |
| 31 | +
|
| 32 | + After installing, ensure: |
| 33 | +
|
| 34 | + * The binaries are root-owned and **not** group- or world-writable. |
| 35 | + The suidhelper refuses binaries with loose permissions. |
| 36 | + * `/etc/hyper/config.toml` is root-owned with mode `0644`. |
| 37 | + """ |
| 38 | + |
| 39 | + use Mix.Task |
| 40 | + |
| 41 | + @version "1.16.0" |
| 42 | + @default_prefix "/opt/firecracker" |
| 43 | + |
| 44 | + @impl Mix.Task |
| 45 | + @spec run([String.t()]) :: :ok |
| 46 | + def run(argv) do |
| 47 | + {opts, _rest, _invalid} = OptionParser.parse(argv, strict: [prefix: :string]) |
| 48 | + prefix = Keyword.get(opts, :prefix, @default_prefix) |
| 49 | + |
| 50 | + arch = detect_arch!() |
| 51 | + |
| 52 | + case Application.ensure_all_started(:req) do |
| 53 | + {:ok, _} -> :ok |
| 54 | + {:error, {reason, app}} -> Mix.raise("Cannot start HTTP client #{app}: #{inspect(reason)}") |
| 55 | + end |
| 56 | + |
| 57 | + install!(release_for(arch), prefix) |
| 58 | + print_config(prefix) |
| 59 | + end |
| 60 | + |
| 61 | + defp detect_arch! do |
| 62 | + case Sys.Arch.current() do |
| 63 | + {:ok, arch} -> |
| 64 | + arch |
| 65 | + |
| 66 | + {:error, {:unsupported_arch, raw}} -> |
| 67 | + Mix.raise( |
| 68 | + "Unsupported CPU architecture #{inspect(raw)}; " <> |
| 69 | + "Firecracker supports x86_64 and aarch64." |
| 70 | + ) |
| 71 | + end |
| 72 | + end |
| 73 | + |
| 74 | + defp release_for(:x86_64) do |
| 75 | + %{ |
| 76 | + url: |
| 77 | + "https://github.com/firecracker-microvm/firecracker/releases/download/" <> |
| 78 | + "v#{@version}/firecracker-v#{@version}-x86_64.tgz", |
| 79 | + sha256: "bd04e26952d4e158085778c6230a0b383d2619c319182e27eaa9d61a212e92d6", |
| 80 | + firecracker_path: "release-v#{@version}-x86_64/firecracker-v#{@version}-x86_64", |
| 81 | + jailer_path: "release-v#{@version}-x86_64/jailer-v#{@version}-x86_64" |
| 82 | + } |
| 83 | + end |
| 84 | + |
| 85 | + defp release_for(:aarch64) do |
| 86 | + %{ |
| 87 | + url: |
| 88 | + "https://github.com/firecracker-microvm/firecracker/releases/download/" <> |
| 89 | + "v#{@version}/firecracker-v#{@version}-aarch64.tgz", |
| 90 | + sha256: "531c713cdbc37d4b8bc2533d851aabc0267096afa1768086a37672abb668efd7", |
| 91 | + firecracker_path: "release-v#{@version}-aarch64/firecracker-v#{@version}-aarch64", |
| 92 | + jailer_path: "release-v#{@version}-aarch64/jailer-v#{@version}-aarch64" |
| 93 | + } |
| 94 | + end |
| 95 | + |
| 96 | + defp install!( |
| 97 | + %{url: url, sha256: sha256, firecracker_path: fc_rel, jailer_path: jailer_rel}, |
| 98 | + prefix |
| 99 | + ) do |
| 100 | + extract_dir = Path.join(prefix, ".firecracker-extract") |
| 101 | + |
| 102 | + Mix.shell().info("Downloading Firecracker v#{@version} from #{url} ...") |
| 103 | + |
| 104 | + case Redist.Targz.install(url, sha256, extract_dir) do |
| 105 | + :ok -> :ok |
| 106 | + {:error, reason} -> Mix.raise("Download from #{url} failed: #{inspect(reason)}") |
| 107 | + end |
| 108 | + |
| 109 | + dst_fc = Path.join(prefix, "firecracker") |
| 110 | + dst_jailer = Path.join(prefix, "jailer") |
| 111 | + |
| 112 | + # The release ships version-stamped names; copy to bare basenames so SafeBin |
| 113 | + # validation passes. The helper matches on basename, not full path. |
| 114 | + File.cp!(Path.join(extract_dir, fc_rel), dst_fc) |
| 115 | + File.cp!(Path.join(extract_dir, jailer_rel), dst_jailer) |
| 116 | + File.chmod!(dst_fc, 0o755) |
| 117 | + File.chmod!(dst_jailer, 0o755) |
| 118 | + _ = File.rm_rf!(extract_dir) |
| 119 | + |
| 120 | + Mix.shell().info("Installed #{dst_fc}") |
| 121 | + Mix.shell().info("Installed #{dst_jailer}") |
| 122 | + end |
| 123 | + |
| 124 | + defp print_config(prefix) do |
| 125 | + fc = Path.join(prefix, "firecracker") |
| 126 | + jailer = Path.join(prefix, "jailer") |
| 127 | + |
| 128 | + # This task runs unprivileged, so the binaries land owned by the invoking |
| 129 | + # user. The suidhelper's SafeBin refuses any binary not owned by root and not |
| 130 | + # free of group/other write bits, so the operator MUST chown/chmod them or |
| 131 | + # every jailer launch fails closed. Print the exact commands rather than a |
| 132 | + # vague "ensure root-owned". |
| 133 | + Mix.shell().info(""" |
| 134 | +
|
| 135 | + Almost done. Run these as root so the setuid helper will accept the binaries |
| 136 | + (it refuses any jailer/firecracker not owned by root): |
| 137 | +
|
| 138 | + sudo chown root:root #{fc} #{jailer} |
| 139 | + sudo chmod 0755 #{fc} #{jailer} |
| 140 | +
|
| 141 | + Then add to /etc/hyper/config.toml (file: root-owned, mode 0644): |
| 142 | +
|
| 143 | + [tools] |
| 144 | + firecracker = "#{fc}" |
| 145 | + jailer = "#{jailer}" |
| 146 | + """) |
| 147 | + end |
| 148 | +end |
0 commit comments